Ticket #1465: 20110829dastodisk.darcs.patch

File 20110829dastodisk.darcs.patch, 130.9 KB (added by zancas, at 2011-08-29T18:53:38Z)

This patch changes the older "das" term to the current "disk" as a backend type.

Line 
1Tue Aug  9 13:39:10 MDT 2011  wilcoxjg@gmail.com
2  * storage: add tests of the new feature of having the storage backend in a separate object from the server
3
4Tue Aug  9 14:09:29 MDT 2011  wilcoxjg@gmail.com
5  * Added directories and new modules for the null backend
6
7Tue Aug  9 14:12:49 MDT 2011  wilcoxjg@gmail.com
8  * changes to null/core.py and storage/common.py necessary for test with null backend to pass
9
10Tue Aug  9 14:16:47 MDT 2011  wilcoxjg@gmail.com
11  * change storage/server.py to new "backend pluggable" version
12
13Tue Aug  9 14:18:22 MDT 2011  wilcoxjg@gmail.com
14  * modify null/core.py such that the correct interfaces are implemented
15
16Tue Aug  9 14:22:32 MDT 2011  wilcoxjg@gmail.com
17  * make changes to storage/immutable.py most changes are part of movement to DAS specific backend.
18
19Tue Aug  9 14:26:20 MDT 2011  wilcoxjg@gmail.com
20  * creates backends/das/core.py
21
22Tue Aug  9 14:31:23 MDT 2011  wilcoxjg@gmail.com
23  * change backends/das/core.py to correct which interfaces are implemented
24
25Tue Aug  9 14:33:21 MDT 2011  wilcoxjg@gmail.com
26  * util/fileutil.py now expects and manipulates twisted.python.filepath.FilePath objects
27
28Tue Aug  9 14:35:19 MDT 2011  wilcoxjg@gmail.com
29  * add expirer.py
30
31Tue Aug  9 14:38:11 MDT 2011  wilcoxjg@gmail.com
32  * Changes I have made that aren't necessary for the test_backends.py suite to pass.
33
34Tue Aug  9 21:37:51 MDT 2011  wilcoxjg@gmail.com
35  * add __init__.py to backend and core and null
36
37Wed Aug 10 11:08:47 MDT 2011  wilcoxjg@gmail.com
38  * whitespace-cleanup
39
40Wed Aug 10 11:38:49 MDT 2011  wilcoxjg@gmail.com
41  * das/__init__.py
42
43Wed Aug 10 14:10:41 MDT 2011  wilcoxjg@gmail.com
44  * test_backends.py: cleaned whitespace and removed unused variables
45
46Mon Aug 29 12:48:34 MDT 2011  wilcoxjg@gmail.com
47  * test_backends.py, backends/das -> backends/disk: renaming backend das to disk
48
49New patches:
50
51[storage: add tests of the new feature of having the storage backend in a separate object from the server
52wilcoxjg@gmail.com**20110809193910
53 Ignore-this: 72b64dab1a9ce668607a4ece4429e29a
54] {
55addfile ./src/allmydata/test/test_backends.py
56hunk ./src/allmydata/test/test_backends.py 1
57+import os, stat
58+from twisted.trial import unittest
59+from allmydata.util.log import msg
60+from allmydata.test.common_util import ReallyEqualMixin
61+import mock
62+# This is the code that we're going to be testing.
63+from allmydata.storage.server import StorageServer
64+from allmydata.storage.backends.das.core import DASCore
65+from allmydata.storage.backends.null.core import NullCore
66+from allmydata.storage.common import si_si2dir
67+# The following share file content was generated with
68+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
69+# with share data == 'a'. The total size of this input
70+# is 85 bytes.
71+shareversionnumber = '\x00\x00\x00\x01'
72+sharedatalength = '\x00\x00\x00\x01'
73+numberofleases = '\x00\x00\x00\x01'
74+shareinputdata = 'a'
75+ownernumber = '\x00\x00\x00\x00'
76+renewsecret  = 'x'*32
77+cancelsecret = 'y'*32
78+expirationtime = '\x00(\xde\x80'
79+nextlease = ''
80+containerdata = shareversionnumber + sharedatalength + numberofleases
81+client_data = shareinputdata + ownernumber + renewsecret + \
82+    cancelsecret + expirationtime + nextlease
83+share_data = containerdata + client_data
84+testnodeid = 'testnodeidxxxxxxxxxx'
85+expiration_policy = {'enabled' : False,
86+                     'mode' : 'age',
87+                     'override_lease_duration' : None,
88+                     'cutoff_date' : None,
89+                     'sharetypes' : None}
90+
91+
92+class MockFileSystem(unittest.TestCase):
93+    """ I simulate a filesystem that the code under test can use. I simulate
94+    just the parts of the filesystem that the current implementation of DAS
95+    backend needs. """
96+    def setUp(self):
97+        # Make patcher, patch, and make effects for fs using functions.
98+        msg( "%s.setUp()" % (self,))
99+        self.mockedfilepaths = {}
100+        #keys are pathnames, values are MockFilePath objects. This is necessary because
101+        #MockFilePath behavior sometimes depends on the filesystem. Where it does,
102+        #self.mockedfilepaths has the relevent info.
103+        self.storedir = MockFilePath('teststoredir', self.mockedfilepaths)
104+        self.basedir = self.storedir.child('shares')
105+        self.baseincdir = self.basedir.child('incoming')
106+        self.sharedirfinalname = self.basedir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
107+        self.sharedirincomingname = self.baseincdir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
108+        self.shareincomingname = self.sharedirincomingname.child('0')
109+        self.sharefinalname = self.sharedirfinalname.child('0')
110+
111+        self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
112+        FakePath = self.FilePathFake.__enter__()
113+
114+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
115+        FakeBCC = self.BCountingCrawler.__enter__()
116+        FakeBCC.side_effect = self.call_FakeBCC
117+
118+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.das.core.LeaseCheckingCrawler')
119+        FakeLCC = self.LeaseCheckingCrawler.__enter__()
120+        FakeLCC.side_effect = self.call_FakeLCC
121+
122+        self.get_available_space = mock.patch('allmydata.util.fileutil.get_available_space')
123+        GetSpace = self.get_available_space.__enter__()
124+        GetSpace.side_effect = self.call_get_available_space
125+
126+        self.statforsize = mock.patch('allmydata.storage.backends.das.core.filepath.stat')
127+        getsize = self.statforsize.__enter__()
128+        getsize.side_effect = self.call_statforsize
129+
130+    def call_FakeBCC(self, StateFile):
131+        return MockBCC()
132+
133+    def call_FakeLCC(self, StateFile, HistoryFile, ExpirationPolicy):
134+        return MockLCC()
135+
136+    def call_get_available_space(self, storedir, reservedspace):
137+        # The input vector has an input size of 85.
138+        return 85 - reservedspace
139+
140+    def call_statforsize(self, fakefpname):
141+        return self.mockedfilepaths[fakefpname].fileobject.size()
142+
143+    def tearDown(self):
144+        msg( "%s.tearDown()" % (self,))
145+        FakePath = self.FilePathFake.__exit__()       
146+        self.mockedfilepaths = {}
147+
148+
149+class MockFilePath:
150+    def __init__(self, pathstring, ffpathsenvironment, existance=False):
151+        #  I can't jsut make the values MockFileObjects because they may be directories.
152+        self.mockedfilepaths = ffpathsenvironment
153+        self.path = pathstring
154+        self.existance = existance
155+        if not self.mockedfilepaths.has_key(self.path):
156+            #  The first MockFilePath object is special
157+            self.mockedfilepaths[self.path] = self
158+            self.fileobject = None
159+        else:
160+            self.fileobject = self.mockedfilepaths[self.path].fileobject
161+        self.spawn = {}
162+        self.antecedent = os.path.dirname(self.path)
163+
164+    def setContent(self, contentstring):
165+        # This method rewrites the data in the file that corresponds to its path
166+        # name whether it preexisted or not.
167+        self.fileobject = MockFileObject(contentstring)
168+        self.existance = True
169+        self.mockedfilepaths[self.path].fileobject = self.fileobject
170+        self.mockedfilepaths[self.path].existance = self.existance
171+        self.setparents()
172+       
173+    def create(self):
174+        # This method chokes if there's a pre-existing file!
175+        if self.mockedfilepaths[self.path].fileobject:
176+            raise OSError
177+        else:
178+            self.fileobject = MockFileObject(contentstring)
179+            self.existance = True
180+            self.mockedfilepaths[self.path].fileobject = self.fileobject
181+            self.mockedfilepaths[self.path].existance = self.existance
182+            self.setparents()       
183+
184+    def open(self, mode='r'):
185+        # XXX Makes no use of mode.
186+        if not self.mockedfilepaths[self.path].fileobject:
187+            # If there's no fileobject there already then make one and put it there.
188+            self.fileobject = MockFileObject()
189+            self.existance = True
190+            self.mockedfilepaths[self.path].fileobject = self.fileobject
191+            self.mockedfilepaths[self.path].existance = self.existance
192+        else:
193+            # Otherwise get a ref to it.
194+            self.fileobject = self.mockedfilepaths[self.path].fileobject
195+            self.existance = self.mockedfilepaths[self.path].existance
196+        return self.fileobject.open(mode)
197+
198+    def child(self, childstring):
199+        arg2child = os.path.join(self.path, childstring)
200+        child = MockFilePath(arg2child, self.mockedfilepaths)
201+        return child
202+
203+    def children(self):
204+        childrenfromffs = [ffp for ffp in self.mockedfilepaths.values() if ffp.path.startswith(self.path)]
205+        childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
206+        childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
207+        self.spawn = frozenset(childrenfromffs)
208+        return self.spawn 
209+
210+    def parent(self):
211+        if self.mockedfilepaths.has_key(self.antecedent):
212+            parent = self.mockedfilepaths[self.antecedent]
213+        else:
214+            parent = MockFilePath(self.antecedent, self.mockedfilepaths)
215+        return parent
216+
217+    def parents(self):
218+        antecedents = []
219+        def f(fps, antecedents):
220+            newfps = os.path.split(fps)[0]
221+            if newfps:
222+                antecedents.append(newfps)
223+                f(newfps, antecedents)
224+        f(self.path, antecedents)
225+        return antecedents
226+
227+    def setparents(self):
228+        for fps in self.parents():
229+            if not self.mockedfilepaths.has_key(fps):
230+                self.mockedfilepaths[fps] = MockFilePath(fps, self.mockedfilepaths, exists=True)
231+
232+    def basename(self):
233+        return os.path.split(self.path)[1]
234+
235+    def moveTo(self, newffp):
236+        #  XXX Makes no distinction between file and directory arguments, this is deviation from filepath.moveTo
237+        if self.mockedfilepaths[newffp.path].exists():
238+            raise OSError
239+        else:
240+            self.mockedfilepaths[newffp.path] = self
241+            self.path = newffp.path
242+
243+    def getsize(self):
244+        return self.fileobject.getsize()
245+
246+    def exists(self):
247+        return self.existance
248+
249+    def isdir(self):
250+        return True
251+
252+    def makedirs(self):
253+        # XXX These methods assume that fp_<FOO> functions in fileutil will be tested elsewhere!
254+        pass
255+
256+    def remove(self):
257+        pass
258+
259+
260+class MockFileObject:
261+    def __init__(self, contentstring=''):
262+        self.buffer = contentstring
263+        self.pos = 0
264+    def open(self, mode='r'):
265+        return self
266+    def write(self, instring):
267+        begin = self.pos
268+        padlen = begin - len(self.buffer)
269+        if padlen > 0:
270+            self.buffer += '\x00' * padlen
271+        end = self.pos + len(instring)
272+        self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
273+        self.pos = end
274+    def close(self):
275+        self.pos = 0
276+    def seek(self, pos):
277+        self.pos = pos
278+    def read(self, numberbytes):
279+        return self.buffer[self.pos:self.pos+numberbytes]
280+    def tell(self):
281+        return self.pos
282+    def size(self):
283+        # XXX This method A: Is not to be found in a real file B: Is part of a wild-mung-up of filepath.stat!
284+        # XXX Finally we shall hopefully use a getsize method soon, must consult first though.
285+        # Hmmm...  perhaps we need to sometimes stat the address when there's not a mockfileobject present?
286+        return {stat.ST_SIZE:len(self.buffer)}
287+    def getsize(self):
288+        return len(self.buffer)
289+
290+class MockBCC:
291+    def setServiceParent(self, Parent):
292+        pass
293+
294+
295+class MockLCC:
296+    def setServiceParent(self, Parent):
297+        pass
298+
299+
300+class TestServerWithNullBackend(unittest.TestCase, ReallyEqualMixin):
301+    """ NullBackend is just for testing and executable documentation, so
302+    this test is actually a test of StorageServer in which we're using
303+    NullBackend as helper code for the test, rather than a test of
304+    NullBackend. """
305+    def setUp(self):
306+        self.ss = StorageServer(testnodeid, backend=NullCore())
307+
308+    @mock.patch('os.mkdir')
309+    @mock.patch('__builtin__.open')
310+    @mock.patch('os.listdir')
311+    @mock.patch('os.path.isdir')
312+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
313+        """ Write a new share. """
314+
315+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
316+        bs[0].remote_write(0, 'a')
317+        self.failIf(mockisdir.called)
318+        self.failIf(mocklistdir.called)
319+        self.failIf(mockopen.called)
320+        self.failIf(mockmkdir.called)
321+
322+
323+class TestServerConstruction(MockFileSystem, ReallyEqualMixin):
324+    def test_create_server_fs_backend(self):
325+        """ This tests whether a server instance can be constructed with a
326+        filesystem backend. To pass the test, it mustn't use the filesystem
327+        outside of its configured storedir. """
328+        StorageServer(testnodeid, backend=DASCore(self.storedir, expiration_policy))
329+
330+
331+class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
332+    """ This tests both the StorageServer and the DAS backend together. """   
333+    def setUp(self):
334+        MockFileSystem.setUp(self)
335+        try:
336+            self.backend = DASCore(self.storedir, expiration_policy)
337+            self.ss = StorageServer(testnodeid, self.backend)
338+
339+            self.backendwithreserve = DASCore(self.storedir, expiration_policy, reserved_space = 1)
340+            self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
341+        except:
342+            MockFileSystem.tearDown(self)
343+            raise
344+
345+    @mock.patch('time.time')
346+    @mock.patch('allmydata.util.fileutil.get_available_space')
347+    def test_out_of_space(self, mockget_available_space, mocktime):
348+        mocktime.return_value = 0
349+       
350+        def call_get_available_space(dir, reserve):
351+            return 0
352+
353+        mockget_available_space.side_effect = call_get_available_space
354+        alreadygotc, bsc = self.sswithreserve.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
355+        self.failUnlessReallyEqual(bsc, {})
356+
357+    @mock.patch('time.time')
358+    def test_write_and_read_share(self, mocktime):
359+        """
360+        Write a new share, read it, and test the server's (and FS backend's)
361+        handling of simultaneous and successive attempts to write the same
362+        share.
363+        """
364+        mocktime.return_value = 0
365+        # Inspect incoming and fail unless it's empty.
366+        incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
367+       
368+        self.failUnlessReallyEqual(incomingset, frozenset())
369+       
370+        # Populate incoming with the sharenum: 0.
371+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
372+
373+        # This is a transparent-box test: Inspect incoming and fail unless the sharenum: 0 is listed there.
374+        self.failUnlessReallyEqual(self.ss.backend.get_incoming_shnums('teststorage_index'), frozenset((0,)))
375+
376+
377+
378+        # Attempt to create a second share writer with the same sharenum.
379+        alreadygota, bsa = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
380+
381+        # Show that no sharewriter results from a remote_allocate_buckets
382+        # with the same si and sharenum, until BucketWriter.remote_close()
383+        # has been called.
384+        self.failIf(bsa)
385+
386+        # Test allocated size.
387+        spaceint = self.ss.allocated_size()
388+        self.failUnlessReallyEqual(spaceint, 1)
389+
390+        # Write 'a' to shnum 0. Only tested together with close and read.
391+        bs[0].remote_write(0, 'a')
392+       
393+        # Preclose: Inspect final, failUnless nothing there.
394+        self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
395+        bs[0].remote_close()
396+
397+        # Postclose: (Omnibus) failUnless written data is in final.
398+        sharesinfinal = list(self.backend.get_shares('teststorage_index'))
399+        self.failUnlessReallyEqual(len(sharesinfinal), 1)
400+        contents = sharesinfinal[0].read_share_data(0, 73)
401+        self.failUnlessReallyEqual(contents, client_data)
402+
403+        # Exercise the case that the share we're asking to allocate is
404+        # already (completely) uploaded.
405+        self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
406+       
407+
408+    def test_read_old_share(self):
409+        """ This tests whether the code correctly finds and reads
410+        shares written out by old (Tahoe-LAFS <= v1.8.2)
411+        servers. There is a similar test in test_download, but that one
412+        is from the perspective of the client and exercises a deeper
413+        stack of code. This one is for exercising just the
414+        StorageServer object. """
415+        # Contruct a file with the appropriate contents in the mockfilesystem.
416+        datalen = len(share_data)
417+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
418+        finalhome.setContent(share_data)
419+
420+        # Now begin the test.
421+        bs = self.ss.remote_get_buckets('teststorage_index')
422+
423+        self.failUnlessEqual(len(bs), 1)
424+        b = bs['0']
425+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
426+        self.failUnlessReallyEqual(b.remote_read(0, datalen), client_data)
427+        # If you try to read past the end you get the as much data as is there.
428+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), client_data)
429+        # If you start reading past the end of the file you get the empty string.
430+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
431}
432[Added directories and new modules for the null backend
433wilcoxjg@gmail.com**20110809200929
434 Ignore-this: f5dfa418afced5141eb9247a9908109e
435] {
436hunk ./src/allmydata/interfaces.py 274
437         store that on disk.
438         """
439 
440+class IStorageBackend(Interface):
441+    """
442+    Objects of this kind live on the server side and are used by the
443+    storage server object.
444+    """
445+    def get_available_space(self, reserved_space):
446+        """ Returns available space for share storage in bytes, or
447+        None if this information is not available or if the available
448+        space is unlimited.
449+
450+        If the backend is configured for read-only mode then this will
451+        return 0.
452+
453+        reserved_space is how many bytes to subtract from the answer, so
454+        you can pass how many bytes you would like to leave unused on this
455+        filesystem as reserved_space. """
456+
457+    def get_bucket_shares(self):
458+        """XXX"""
459+
460+    def get_share(self):
461+        """XXX"""
462+
463+    def make_bucket_writer(self):
464+        """XXX"""
465+
466+class IStorageBackendShare(Interface):
467+    """
468+    This object contains as much as all of the share data.  It is intended
469+    for lazy evaluation such that in many use cases substantially less than
470+    all of the share data will be accessed.
471+    """
472+    def is_complete(self):
473+        """
474+        Returns the share state, or None if the share does not exist.
475+        """
476+
477 class IStorageBucketWriter(Interface):
478     """
479     Objects of this kind live on the client side.
480adddir ./src/allmydata/storage/backends
481addfile ./src/allmydata/storage/backends/base.py
482hunk ./src/allmydata/storage/backends/base.py 1
483+from twisted.application import service
484+
485+class Backend(service.MultiService):
486+    def __init__(self):
487+        service.MultiService.__init__(self)
488adddir ./src/allmydata/storage/backends/null
489addfile ./src/allmydata/storage/backends/null/core.py
490hunk ./src/allmydata/storage/backends/null/core.py 1
491+from allmydata.storage.backends.base import Backend
492+from allmydata.storage.immutable import BucketWriter, BucketReader
493+
494+class NullCore(Backend):
495+    def __init__(self):
496+        Backend.__init__(self)
497+
498+    def get_available_space(self):
499+        return None
500+
501+    def get_shares(self, storage_index):
502+        return set()
503+
504+    def get_share(self, storage_index, sharenum):
505+        return None
506+
507+    def make_bucket_writer(self, storageindex, shnum, max_space_per_bucket, lease_info, canary):
508+        immutableshare = ImmutableShare()
509+        return BucketWriter(self.ss, immutableshare, max_space_per_bucket, lease_info, canary)
510+
511+    def set_storage_server(self, ss):
512+        self.ss = ss
513+
514+    def get_incoming_shnums(self, storageindex):
515+        return frozenset()
516+
517+class ImmutableShare:
518+    sharetype = "immutable"
519+
520+    def __init__(self):
521+        """ If max_size is not None then I won't allow more than
522+        max_size to be written to me. If create=True then max_size
523+        must not be None. """
524+        pass
525+
526+    def get_shnum(self):
527+        return self.shnum
528+
529+    def unlink(self):
530+        os.unlink(self.fname)
531+
532+    def read_share_data(self, offset, length):
533+        precondition(offset >= 0)
534+        # Reads beyond the end of the data are truncated. Reads that start
535+        # beyond the end of the data return an empty string.
536+        seekpos = self._data_offset+offset
537+        fsize = os.path.getsize(self.fname)
538+        actuallength = max(0, min(length, fsize-seekpos))
539+        if actuallength == 0:
540+            return ""
541+        f = open(self.fname, 'rb')
542+        f.seek(seekpos)
543+        return f.read(actuallength)
544+
545+    def write_share_data(self, offset, data):
546+        pass
547+
548+    def _write_lease_record(self, f, lease_number, lease_info):
549+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
550+        f.seek(offset)
551+        assert f.tell() == offset
552+        f.write(lease_info.to_immutable_data())
553+
554+    def _read_num_leases(self, f):
555+        f.seek(0x08)
556+        (num_leases,) = struct.unpack(">L", f.read(4))
557+        return num_leases
558+
559+    def _write_num_leases(self, f, num_leases):
560+        f.seek(0x08)
561+        f.write(struct.pack(">L", num_leases))
562+
563+    def _truncate_leases(self, f, num_leases):
564+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
565+
566+    def get_leases(self):
567+        """Yields a LeaseInfo instance for all leases."""
568+        f = open(self.fname, 'rb')
569+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
570+        f.seek(self._lease_offset)
571+        for i in range(num_leases):
572+            data = f.read(self.LEASE_SIZE)
573+            if data:
574+                yield LeaseInfo().from_immutable_data(data)
575+
576+    def add_lease(self, lease):
577+        pass
578+
579+    def renew_lease(self, renew_secret, new_expire_time):
580+        for i,lease in enumerate(self.get_leases()):
581+            if constant_time_compare(lease.renew_secret, renew_secret):
582+                # yup. See if we need to update the owner time.
583+                if new_expire_time > lease.expiration_time:
584+                    # yes
585+                    lease.expiration_time = new_expire_time
586+                    f = open(self.fname, 'rb+')
587+                    self._write_lease_record(f, i, lease)
588+                    f.close()
589+                return
590+        raise IndexError("unable to renew non-existent lease")
591+
592+    def add_or_renew_lease(self, lease_info):
593+        try:
594+            self.renew_lease(lease_info.renew_secret,
595+                             lease_info.expiration_time)
596+        except IndexError:
597+            self.add_lease(lease_info)
598+
599+
600+    def cancel_lease(self, cancel_secret):
601+        """Remove a lease with the given cancel_secret. If the last lease is
602+        cancelled, the file will be removed. Return the number of bytes that
603+        were freed (by truncating the list of leases, and possibly by
604+        deleting the file. Raise IndexError if there was no lease with the
605+        given cancel_secret.
606+        """
607+
608+        leases = list(self.get_leases())
609+        num_leases_removed = 0
610+        for i,lease in enumerate(leases):
611+            if constant_time_compare(lease.cancel_secret, cancel_secret):
612+                leases[i] = None
613+                num_leases_removed += 1
614+        if not num_leases_removed:
615+            raise IndexError("unable to find matching lease to cancel")
616+        if num_leases_removed:
617+            # pack and write out the remaining leases. We write these out in
618+            # the same order as they were added, so that if we crash while
619+            # doing this, we won't lose any non-cancelled leases.
620+            leases = [l for l in leases if l] # remove the cancelled leases
621+            f = open(self.fname, 'rb+')
622+            for i,lease in enumerate(leases):
623+                self._write_lease_record(f, i, lease)
624+            self._write_num_leases(f, len(leases))
625+            self._truncate_leases(f, len(leases))
626+            f.close()
627+        space_freed = self.LEASE_SIZE * num_leases_removed
628+        if not len(leases):
629+            space_freed += os.stat(self.fname)[stat.ST_SIZE]
630+            self.unlink()
631+        return space_freed
632}
633[changes to null/core.py and storage/common.py necessary for test with null backend to pass
634wilcoxjg@gmail.com**20110809201249
635 Ignore-this: 9ddcd79f9962550ed20518ae85b6b6b2
636] {
637hunk ./src/allmydata/storage/backends/null/core.py 3
638 from allmydata.storage.backends.base import Backend
639 from allmydata.storage.immutable import BucketWriter, BucketReader
640+from zope.interface import implements
641 
642 class NullCore(Backend):
643hunk ./src/allmydata/storage/backends/null/core.py 6
644+    implements(IStorageBackend)
645     def __init__(self):
646         Backend.__init__(self)
647 
648hunk ./src/allmydata/storage/backends/null/core.py 30
649         return frozenset()
650 
651 class ImmutableShare:
652+    implements(IStorageBackendShare)
653     sharetype = "immutable"
654 
655     def __init__(self):
656hunk ./src/allmydata/storage/common.py 19
657 def si_a2b(ascii_storageindex):
658     return base32.a2b(ascii_storageindex)
659 
660-def storage_index_to_dir(storageindex):
661+def si_si2dir(startfp, storageindex):
662     sia = si_b2a(storageindex)
663hunk ./src/allmydata/storage/common.py 21
664-    return os.path.join(sia[:2], sia)
665+    newfp = startfp.child(sia[:2])
666+    return newfp.child(sia)
667}
668[change storage/server.py to new "backend pluggable" version
669wilcoxjg@gmail.com**20110809201647
670 Ignore-this: 1b0c5f9e831641287992bf45af55246e
671] {
672hunk ./src/allmydata/storage/server.py 1
673-import os, re, weakref, struct, time
674+import os, weakref, struct, time
675 
676 from foolscap.api import Referenceable
677 from twisted.application import service
678hunk ./src/allmydata/storage/server.py 11
679 from allmydata.util import fileutil, idlib, log, time_format
680 import allmydata # for __full_version__
681 
682-from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
683-_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
684+from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
685+_pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
686 from allmydata.storage.lease import LeaseInfo
687 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
688      create_mutable_sharefile
689hunk ./src/allmydata/storage/server.py 16
690-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
691-from allmydata.storage.crawler import BucketCountingCrawler
692-from allmydata.storage.expirer import LeaseCheckingCrawler
693-
694-# storage/
695-# storage/shares/incoming
696-#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
697-#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
698-# storage/shares/$START/$STORAGEINDEX
699-# storage/shares/$START/$STORAGEINDEX/$SHARENUM
700-
701-# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
702-# base-32 chars).
703-
704-# $SHARENUM matches this regex:
705-NUM_RE=re.compile("^[0-9]+$")
706-
707-
708 
709 class StorageServer(service.MultiService, Referenceable):
710     implements(RIStorageServer, IStatsProducer)
711hunk ./src/allmydata/storage/server.py 20
712     name = 'storage'
713-    LeaseCheckerClass = LeaseCheckingCrawler
714 
715hunk ./src/allmydata/storage/server.py 21
716-    def __init__(self, storedir, nodeid, reserved_space=0,
717-                 discard_storage=False, readonly_storage=False,
718-                 stats_provider=None,
719-                 expiration_enabled=False,
720-                 expiration_mode="age",
721-                 expiration_override_lease_duration=None,
722-                 expiration_cutoff_date=None,
723-                 expiration_sharetypes=("mutable", "immutable")):
724+    def __init__(self, nodeid, backend, reserved_space=0,
725+                 readonly_storage=False,
726+                 stats_provider=None ):
727         service.MultiService.__init__(self)
728         assert isinstance(nodeid, str)
729         assert len(nodeid) == 20
730hunk ./src/allmydata/storage/server.py 28
731         self.my_nodeid = nodeid
732-        self.storedir = storedir
733-        sharedir = os.path.join(storedir, "shares")
734-        fileutil.make_dirs(sharedir)
735-        self.sharedir = sharedir
736-        # we don't actually create the corruption-advisory dir until necessary
737-        self.corruption_advisory_dir = os.path.join(storedir,
738-                                                    "corruption-advisories")
739-        self.reserved_space = int(reserved_space)
740-        self.no_storage = discard_storage
741-        self.readonly_storage = readonly_storage
742         self.stats_provider = stats_provider
743         if self.stats_provider:
744             self.stats_provider.register_producer(self)
745hunk ./src/allmydata/storage/server.py 31
746-        self.incomingdir = os.path.join(sharedir, 'incoming')
747-        self._clean_incomplete()
748-        fileutil.make_dirs(self.incomingdir)
749         self._active_writers = weakref.WeakKeyDictionary()
750hunk ./src/allmydata/storage/server.py 32
751+        self.backend = backend
752+        self.backend.setServiceParent(self)
753+        self.backend.set_storage_server(self)
754         log.msg("StorageServer created", facility="tahoe.storage")
755 
756hunk ./src/allmydata/storage/server.py 37
757-        if reserved_space:
758-            if self.get_available_space() is None:
759-                log.msg("warning: [storage]reserved_space= is set, but this platform does not support an API to get disk statistics (statvfs(2) or GetDiskFreeSpaceEx), so this reservation cannot be honored",
760-                        umin="0wZ27w", level=log.UNUSUAL)
761-
762         self.latencies = {"allocate": [], # immutable
763                           "write": [],
764                           "close": [],
765hunk ./src/allmydata/storage/server.py 48
766                           "renew": [],
767                           "cancel": [],
768                           }
769-        self.add_bucket_counter()
770-
771-        statefile = os.path.join(self.storedir, "lease_checker.state")
772-        historyfile = os.path.join(self.storedir, "lease_checker.history")
773-        klass = self.LeaseCheckerClass
774-        self.lease_checker = klass(self, statefile, historyfile,
775-                                   expiration_enabled, expiration_mode,
776-                                   expiration_override_lease_duration,
777-                                   expiration_cutoff_date,
778-                                   expiration_sharetypes)
779-        self.lease_checker.setServiceParent(self)
780 
781     def __repr__(self):
782         return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
783hunk ./src/allmydata/storage/server.py 52
784 
785-    def add_bucket_counter(self):
786-        statefile = os.path.join(self.storedir, "bucket_counter.state")
787-        self.bucket_counter = BucketCountingCrawler(self, statefile)
788-        self.bucket_counter.setServiceParent(self)
789-
790     def count(self, name, delta=1):
791         if self.stats_provider:
792             self.stats_provider.count("storage_server." + name, delta)
793hunk ./src/allmydata/storage/server.py 66
794         """Return a dict, indexed by category, that contains a dict of
795         latency numbers for each category. If there are sufficient samples
796         for unambiguous interpretation, each dict will contain the
797-        following keys: mean, 01_0_percentile, 10_0_percentile,
798+        following keys: samplesize, mean, 01_0_percentile, 10_0_percentile,
799         50_0_percentile (median), 90_0_percentile, 95_0_percentile,
800         99_0_percentile, 99_9_percentile.  If there are insufficient
801         samples for a given percentile to be interpreted unambiguously
802hunk ./src/allmydata/storage/server.py 88
803             else:
804                 stats["mean"] = None
805 
806-            orderstatlist = [(0.01, "01_0_percentile", 100), (0.1, "10_0_percentile", 10),\
807-                             (0.50, "50_0_percentile", 10), (0.90, "90_0_percentile", 10),\
808-                             (0.95, "95_0_percentile", 20), (0.99, "99_0_percentile", 100),\
809+            orderstatlist = [(0.1, "10_0_percentile", 10), (0.5, "50_0_percentile", 10), \
810+                             (0.9, "90_0_percentile", 10), (0.95, "95_0_percentile", 20), \
811+                             (0.01, "01_0_percentile", 100),  (0.99, "99_0_percentile", 100),\
812                              (0.999, "99_9_percentile", 1000)]
813 
814             for percentile, percentilestring, minnumtoobserve in orderstatlist:
815hunk ./src/allmydata/storage/server.py 107
816             kwargs["facility"] = "tahoe.storage"
817         return log.msg(*args, **kwargs)
818 
819-    def _clean_incomplete(self):
820-        fileutil.rm_dir(self.incomingdir)
821-
822     def get_stats(self):
823         # remember: RIStatsProvider requires that our return dict
824hunk ./src/allmydata/storage/server.py 109
825-        # contains numeric values.
826+        # contains numeric, or None values.
827         stats = { 'storage_server.allocated': self.allocated_size(), }
828         stats['storage_server.reserved_space'] = self.reserved_space
829         for category,ld in self.get_latencies().items():
830merger 0.0 (
831hunk ./src/allmydata/storage/server.py 149
832-        return fileutil.get_available_space(self.storedir, self.reserved_space)
833+        return fileutil.get_available_space(self.sharedir, self.reserved_space)
834hunk ./src/allmydata/storage/server.py 143
835-    def get_available_space(self):
836-        """Returns available space for share storage in bytes, or None if no
837-        API to get this information is available."""
838-
839-        if self.readonly_storage:
840-            return 0
841-        return fileutil.get_available_space(self.storedir, self.reserved_space)
842-
843)
844hunk ./src/allmydata/storage/server.py 158
845         return space
846 
847     def remote_get_version(self):
848-        remaining_space = self.get_available_space()
849+        remaining_space = self.backend.get_available_space()
850         if remaining_space is None:
851             # We're on a platform that has no API to get disk stats.
852             remaining_space = 2**64
853hunk ./src/allmydata/storage/server.py 172
854                     }
855         return version
856 
857-    def remote_allocate_buckets(self, storage_index,
858+    def remote_allocate_buckets(self, storageindex,
859                                 renew_secret, cancel_secret,
860                                 sharenums, allocated_size,
861                                 canary, owner_num=0):
862hunk ./src/allmydata/storage/server.py 181
863         # to a particular owner.
864         start = time.time()
865         self.count("allocate")
866-        alreadygot = set()
867+        incoming = set()
868         bucketwriters = {} # k: shnum, v: BucketWriter
869hunk ./src/allmydata/storage/server.py 183
870-        si_dir = storage_index_to_dir(storage_index)
871-        si_s = si_b2a(storage_index)
872 
873hunk ./src/allmydata/storage/server.py 184
874+        si_s = si_b2a(storageindex)
875         log.msg("storage: allocate_buckets %s" % si_s)
876 
877         # in this implementation, the lease information (including secrets)
878hunk ./src/allmydata/storage/server.py 198
879 
880         max_space_per_bucket = allocated_size
881 
882-        remaining_space = self.get_available_space()
883+        remaining_space = self.backend.get_available_space()
884         limited = remaining_space is not None
885         if limited:
886             # this is a bit conservative, since some of this allocated_size()
887hunk ./src/allmydata/storage/server.py 207
888             remaining_space -= self.allocated_size()
889         # self.readonly_storage causes remaining_space <= 0
890 
891-        # fill alreadygot with all shares that we have, not just the ones
892+        # Fill alreadygot with all shares that we have, not just the ones
893         # they asked about: this will save them a lot of work. Add or update
894         # leases for all of them: if they want us to hold shares for this
895hunk ./src/allmydata/storage/server.py 210
896-        # file, they'll want us to hold leases for this file.
897-        for (shnum, fn) in self._get_bucket_shares(storage_index):
898-            alreadygot.add(shnum)
899-            sf = ShareFile(fn)
900-            sf.add_or_renew_lease(lease_info)
901+        # file, they'll want us to hold leases for all the shares of it.
902+        alreadygot = set()
903+        for share in self.backend.get_shares(storageindex):
904+            share.add_or_renew_lease(lease_info)
905+            alreadygot.add(share.shnum)
906 
907hunk ./src/allmydata/storage/server.py 216
908-        for shnum in sharenums:
909-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
910-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
911-            if os.path.exists(finalhome):
912-                # great! we already have it. easy.
913-                pass
914-            elif os.path.exists(incominghome):
915-                # Note that we don't create BucketWriters for shnums that
916-                # have a partial share (in incoming/), so if a second upload
917-                # occurs while the first is still in progress, the second
918-                # uploader will use different storage servers.
919-                pass
920-            elif (not limited) or (remaining_space >= max_space_per_bucket):
921-                # ok! we need to create the new share file.
922-                bw = BucketWriter(self, incominghome, finalhome,
923-                                  max_space_per_bucket, lease_info, canary)
924-                if self.no_storage:
925-                    bw.throw_out_all_data = True
926+        # all share numbers that are incoming
927+        incoming = self.backend.get_incoming_shnums(storageindex)
928+
929+        for shnum in ((sharenums - alreadygot) - incoming):
930+            if (not limited) or (remaining_space >= max_space_per_bucket):
931+                bw = self.backend.make_bucket_writer(storageindex, shnum, max_space_per_bucket, lease_info, canary)
932                 bucketwriters[shnum] = bw
933                 self._active_writers[bw] = 1
934                 if limited:
935hunk ./src/allmydata/storage/server.py 227
936                     remaining_space -= max_space_per_bucket
937             else:
938-                # bummer! not enough space to accept this bucket
939+                # Bummer not enough space to accept this share.
940                 pass
941 
942hunk ./src/allmydata/storage/server.py 230
943-        if bucketwriters:
944-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
945-
946         self.add_latency("allocate", time.time() - start)
947         return alreadygot, bucketwriters
948 
949hunk ./src/allmydata/storage/server.py 233
950-    def _iter_share_files(self, storage_index):
951-        for shnum, filename in self._get_bucket_shares(storage_index):
952+    def _iter_share_files(self, storageindex):
953+        for shnum, filename in self._get_shares(storageindex):
954             f = open(filename, 'rb')
955             header = f.read(32)
956             f.close()
957hunk ./src/allmydata/storage/server.py 239
958             if header[:32] == MutableShareFile.MAGIC:
959+                # XXX  Can I exploit this code?
960                 sf = MutableShareFile(filename, self)
961                 # note: if the share has been migrated, the renew_lease()
962                 # call will throw an exception, with information to help the
963hunk ./src/allmydata/storage/server.py 245
964                 # client update the lease.
965             elif header[:4] == struct.pack(">L", 1):
966+                # Check if version number is "1".
967+                # XXX WHAT ABOUT OTHER VERSIONS!!!!!!!?
968                 sf = ShareFile(filename)
969             else:
970                 continue # non-sharefile
971hunk ./src/allmydata/storage/server.py 252
972             yield sf
973 
974-    def remote_add_lease(self, storage_index, renew_secret, cancel_secret,
975+    def remote_add_lease(self, storageindex, renew_secret, cancel_secret,
976                          owner_num=1):
977         start = time.time()
978         self.count("add-lease")
979hunk ./src/allmydata/storage/server.py 260
980         lease_info = LeaseInfo(owner_num,
981                                renew_secret, cancel_secret,
982                                new_expire_time, self.my_nodeid)
983-        for sf in self._iter_share_files(storage_index):
984+        for sf in self._iter_share_files(storageindex):
985             sf.add_or_renew_lease(lease_info)
986         self.add_latency("add-lease", time.time() - start)
987         return None
988hunk ./src/allmydata/storage/server.py 265
989 
990-    def remote_renew_lease(self, storage_index, renew_secret):
991+    def remote_renew_lease(self, storageindex, renew_secret):
992         start = time.time()
993         self.count("renew")
994         new_expire_time = time.time() + 31*24*60*60
995hunk ./src/allmydata/storage/server.py 270
996         found_buckets = False
997-        for sf in self._iter_share_files(storage_index):
998+        for sf in self._iter_share_files(storageindex):
999             found_buckets = True
1000             sf.renew_lease(renew_secret, new_expire_time)
1001         self.add_latency("renew", time.time() - start)
1002hunk ./src/allmydata/storage/server.py 277
1003         if not found_buckets:
1004             raise IndexError("no such lease to renew")
1005 
1006-    def remote_cancel_lease(self, storage_index, cancel_secret):
1007+    def remote_cancel_lease(self, storageindex, cancel_secret):
1008         start = time.time()
1009         self.count("cancel")
1010 
1011hunk ./src/allmydata/storage/server.py 283
1012         total_space_freed = 0
1013         found_buckets = False
1014-        for sf in self._iter_share_files(storage_index):
1015+        for sf in self._iter_share_files(storageindex):
1016             # note: if we can't find a lease on one share, we won't bother
1017             # looking in the others. Unless something broke internally
1018             # (perhaps we ran out of disk space while adding a lease), the
1019hunk ./src/allmydata/storage/server.py 293
1020             total_space_freed += sf.cancel_lease(cancel_secret)
1021 
1022         if found_buckets:
1023-            storagedir = os.path.join(self.sharedir,
1024-                                      storage_index_to_dir(storage_index))
1025-            if not os.listdir(storagedir):
1026-                os.rmdir(storagedir)
1027+            # XXX  Yikes looks like code that shouldn't be in the server!
1028+            storagedir = si_si2dir(self.sharedir, storageindex)
1029+            fp_rmdir_if_empty(storagedir)
1030 
1031         if self.stats_provider:
1032             self.stats_provider.count('storage_server.bytes_freed',
1033hunk ./src/allmydata/storage/server.py 309
1034             self.stats_provider.count('storage_server.bytes_added', consumed_size)
1035         del self._active_writers[bw]
1036 
1037-    def _get_bucket_shares(self, storage_index):
1038-        """Return a list of (shnum, pathname) tuples for files that hold
1039-        shares for this storage_index. In each tuple, 'shnum' will always be
1040-        the integer form of the last component of 'pathname'."""
1041-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
1042-        try:
1043-            for f in os.listdir(storagedir):
1044-                if NUM_RE.match(f):
1045-                    filename = os.path.join(storagedir, f)
1046-                    yield (int(f), filename)
1047-        except OSError:
1048-            # Commonly caused by there being no buckets at all.
1049-            pass
1050-
1051-    def remote_get_buckets(self, storage_index):
1052+    def remote_get_buckets(self, storageindex):
1053         start = time.time()
1054         self.count("get")
1055hunk ./src/allmydata/storage/server.py 312
1056-        si_s = si_b2a(storage_index)
1057+        si_s = si_b2a(storageindex)
1058         log.msg("storage: get_buckets %s" % si_s)
1059         bucketreaders = {} # k: sharenum, v: BucketReader
1060hunk ./src/allmydata/storage/server.py 315
1061-        for shnum, filename in self._get_bucket_shares(storage_index):
1062-            bucketreaders[shnum] = BucketReader(self, filename,
1063-                                                storage_index, shnum)
1064+        self.backend.set_storage_server(self)
1065+        for share in self.backend.get_shares(storageindex):
1066+            bucketreaders[share.get_shnum()] = self.backend.make_bucket_reader(share)
1067         self.add_latency("get", time.time() - start)
1068         return bucketreaders
1069 
1070hunk ./src/allmydata/storage/server.py 321
1071-    def get_leases(self, storage_index):
1072+    def get_leases(self, storageindex):
1073         """Provide an iterator that yields all of the leases attached to this
1074         bucket. Each lease is returned as a LeaseInfo instance.
1075 
1076hunk ./src/allmydata/storage/server.py 331
1077         # since all shares get the same lease data, we just grab the leases
1078         # from the first share
1079         try:
1080-            shnum, filename = self._get_bucket_shares(storage_index).next()
1081+            shnum, filename = self._get_shares(storageindex).next()
1082             sf = ShareFile(filename)
1083             return sf.get_leases()
1084         except StopIteration:
1085hunk ./src/allmydata/storage/server.py 337
1086             return iter([])
1087 
1088-    def remote_slot_testv_and_readv_and_writev(self, storage_index,
1089+    #  XXX  As far as Zancas' grockery has gotten.
1090+    def remote_slot_testv_and_readv_and_writev(self, storageindex,
1091                                                secrets,
1092                                                test_and_write_vectors,
1093                                                read_vector):
1094hunk ./src/allmydata/storage/server.py 344
1095         start = time.time()
1096         self.count("writev")
1097-        si_s = si_b2a(storage_index)
1098+        si_s = si_b2a(storageindex)
1099         log.msg("storage: slot_writev %s" % si_s)
1100hunk ./src/allmydata/storage/server.py 346
1101-        si_dir = storage_index_to_dir(storage_index)
1102+       
1103         (write_enabler, renew_secret, cancel_secret) = secrets
1104         # shares exist if there is a file for them
1105hunk ./src/allmydata/storage/server.py 349
1106-        bucketdir = os.path.join(self.sharedir, si_dir)
1107+        bucketdir = si_si2dir(self.sharedir, storageindex)
1108         shares = {}
1109         if os.path.isdir(bucketdir):
1110             for sharenum_s in os.listdir(bucketdir):
1111hunk ./src/allmydata/storage/server.py 432
1112                                          self)
1113         return share
1114 
1115-    def remote_slot_readv(self, storage_index, shares, readv):
1116+    def remote_slot_readv(self, storageindex, shares, readv):
1117         start = time.time()
1118         self.count("readv")
1119hunk ./src/allmydata/storage/server.py 435
1120-        si_s = si_b2a(storage_index)
1121+        si_s = si_b2a(storageindex)
1122         lp = log.msg("storage: slot_readv %s %s" % (si_s, shares),
1123                      facility="tahoe.storage", level=log.OPERATIONAL)
1124hunk ./src/allmydata/storage/server.py 438
1125-        si_dir = storage_index_to_dir(storage_index)
1126         # shares exist if there is a file for them
1127hunk ./src/allmydata/storage/server.py 439
1128-        bucketdir = os.path.join(self.sharedir, si_dir)
1129+        bucketdir = si_si2dir(self.sharedir, storageindex)
1130         if not os.path.isdir(bucketdir):
1131             self.add_latency("readv", time.time() - start)
1132             return {}
1133hunk ./src/allmydata/storage/server.py 458
1134         self.add_latency("readv", time.time() - start)
1135         return datavs
1136 
1137-    def remote_advise_corrupt_share(self, share_type, storage_index, shnum,
1138+    def remote_advise_corrupt_share(self, share_type, storageindex, shnum,
1139                                     reason):
1140         fileutil.make_dirs(self.corruption_advisory_dir)
1141         now = time_format.iso_utc(sep="T")
1142hunk ./src/allmydata/storage/server.py 462
1143-        si_s = si_b2a(storage_index)
1144+        si_s = si_b2a(storageindex)
1145         # windows can't handle colons in the filename
1146         fn = os.path.join(self.corruption_advisory_dir,
1147                           "%s--%s-%d" % (now, si_s, shnum)).replace(":","")
1148hunk ./src/allmydata/storage/server.py 469
1149         f = open(fn, "w")
1150         f.write("report: Share Corruption\n")
1151         f.write("type: %s\n" % share_type)
1152-        f.write("storage_index: %s\n" % si_s)
1153+        f.write("storageindex: %s\n" % si_s)
1154         f.write("share_number: %d\n" % shnum)
1155         f.write("\n")
1156         f.write(reason)
1157}
1158[modify null/core.py such that the correct interfaces are implemented
1159wilcoxjg@gmail.com**20110809201822
1160 Ignore-this: 3c64580592474f71633287d1b6beeb6b
1161] hunk ./src/allmydata/storage/backends/null/core.py 4
1162 from allmydata.storage.backends.base import Backend
1163 from allmydata.storage.immutable import BucketWriter, BucketReader
1164 from zope.interface import implements
1165+from allmydata.interfaces import IStorageBackend, IStorageBackendShare
1166 
1167 class NullCore(Backend):
1168     implements(IStorageBackend)
1169[make changes to storage/immutable.py most changes are part of movement to DAS specific backend.
1170wilcoxjg@gmail.com**20110809202232
1171 Ignore-this: 70c7c6ea6be2418d70556718a050714
1172] {
1173hunk ./src/allmydata/storage/immutable.py 1
1174-import os, stat, struct, time
1175+import os, time
1176 
1177 from foolscap.api import Referenceable
1178 
1179hunk ./src/allmydata/storage/immutable.py 7
1180 from zope.interface import implements
1181 from allmydata.interfaces import RIBucketWriter, RIBucketReader
1182-from allmydata.util import base32, fileutil, log
1183+from allmydata.util import base32, log
1184 from allmydata.util.assertutil import precondition
1185 from allmydata.util.hashutil import constant_time_compare
1186 from allmydata.storage.lease import LeaseInfo
1187hunk ./src/allmydata/storage/immutable.py 14
1188 from allmydata.storage.common import UnknownImmutableContainerVersionError, \
1189      DataTooLargeError
1190 
1191-# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1192-# and share data. The share data is accessed by RIBucketWriter.write and
1193-# RIBucketReader.read . The lease information is not accessible through these
1194-# interfaces.
1195-
1196-# The share file has the following layout:
1197-#  0x00: share file version number, four bytes, current version is 1
1198-#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1199-#  0x08: number of leases, four bytes big-endian
1200-#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1201-#  A+0x0c = B: first lease. Lease format is:
1202-#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1203-#   B+0x04: renew secret, 32 bytes (SHA256)
1204-#   B+0x24: cancel secret, 32 bytes (SHA256)
1205-#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1206-#   B+0x48: next lease, or end of record
1207-
1208-# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1209-# but it is still filled in by storage servers in case the storage server
1210-# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1211-# share file is moved from one storage server to another. The value stored in
1212-# this field is truncated, so if the actual share data length is >= 2**32,
1213-# then the value stored in this field will be the actual share data length
1214-# modulo 2**32.
1215-
1216-class ShareFile:
1217-    LEASE_SIZE = struct.calcsize(">L32s32sL")
1218-    sharetype = "immutable"
1219-
1220-    def __init__(self, filename, max_size=None, create=False):
1221-        """ If max_size is not None then I won't allow more than max_size to be written to me. If create=True and max_size must not be None. """
1222-        precondition((max_size is not None) or (not create), max_size, create)
1223-        self.home = filename
1224-        self._max_size = max_size
1225-        if create:
1226-            # touch the file, so later callers will see that we're working on
1227-            # it. Also construct the metadata.
1228-            assert not os.path.exists(self.home)
1229-            fileutil.make_dirs(os.path.dirname(self.home))
1230-            f = open(self.home, 'wb')
1231-            # The second field -- the four-byte share data length -- is no
1232-            # longer used as of Tahoe v1.3.0, but we continue to write it in
1233-            # there in case someone downgrades a storage server from >=
1234-            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1235-            # server to another, etc. We do saturation -- a share data length
1236-            # larger than 2**32-1 (what can fit into the field) is marked as
1237-            # the largest length that can fit into the field. That way, even
1238-            # if this does happen, the old < v1.3.0 server will still allow
1239-            # clients to read the first part of the share.
1240-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1241-            f.close()
1242-            self._lease_offset = max_size + 0x0c
1243-            self._num_leases = 0
1244-        else:
1245-            f = open(self.home, 'rb')
1246-            filesize = os.path.getsize(self.home)
1247-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1248-            f.close()
1249-            if version != 1:
1250-                msg = "sharefile %s had version %d but we wanted 1" % \
1251-                      (filename, version)
1252-                raise UnknownImmutableContainerVersionError(msg)
1253-            self._num_leases = num_leases
1254-            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1255-        self._data_offset = 0xc
1256-
1257-    def unlink(self):
1258-        os.unlink(self.home)
1259-
1260-    def read_share_data(self, offset, length):
1261-        precondition(offset >= 0)
1262-        # reads beyond the end of the data are truncated. Reads that start
1263-        # beyond the end of the data return an empty string. I wonder why
1264-        # Python doesn't do the following computation for me?
1265-        seekpos = self._data_offset+offset
1266-        fsize = os.path.getsize(self.home)
1267-        actuallength = max(0, min(length, fsize-seekpos))
1268-        if actuallength == 0:
1269-            return ""
1270-        f = open(self.home, 'rb')
1271-        f.seek(seekpos)
1272-        return f.read(actuallength)
1273-
1274-    def write_share_data(self, offset, data):
1275-        length = len(data)
1276-        precondition(offset >= 0, offset)
1277-        if self._max_size is not None and offset+length > self._max_size:
1278-            raise DataTooLargeError(self._max_size, offset, length)
1279-        f = open(self.home, 'rb+')
1280-        real_offset = self._data_offset+offset
1281-        f.seek(real_offset)
1282-        assert f.tell() == real_offset
1283-        f.write(data)
1284-        f.close()
1285-
1286-    def _write_lease_record(self, f, lease_number, lease_info):
1287-        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1288-        f.seek(offset)
1289-        assert f.tell() == offset
1290-        f.write(lease_info.to_immutable_data())
1291-
1292-    def _read_num_leases(self, f):
1293-        f.seek(0x08)
1294-        (num_leases,) = struct.unpack(">L", f.read(4))
1295-        return num_leases
1296-
1297-    def _write_num_leases(self, f, num_leases):
1298-        f.seek(0x08)
1299-        f.write(struct.pack(">L", num_leases))
1300-
1301-    def _truncate_leases(self, f, num_leases):
1302-        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1303-
1304-    def get_leases(self):
1305-        """Yields a LeaseInfo instance for all leases."""
1306-        f = open(self.home, 'rb')
1307-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1308-        f.seek(self._lease_offset)
1309-        for i in range(num_leases):
1310-            data = f.read(self.LEASE_SIZE)
1311-            if data:
1312-                yield LeaseInfo().from_immutable_data(data)
1313-
1314-    def add_lease(self, lease_info):
1315-        f = open(self.home, 'rb+')
1316-        num_leases = self._read_num_leases(f)
1317-        self._write_lease_record(f, num_leases, lease_info)
1318-        self._write_num_leases(f, num_leases+1)
1319-        f.close()
1320-
1321-    def renew_lease(self, renew_secret, new_expire_time):
1322-        for i,lease in enumerate(self.get_leases()):
1323-            if constant_time_compare(lease.renew_secret, renew_secret):
1324-                # yup. See if we need to update the owner time.
1325-                if new_expire_time > lease.expiration_time:
1326-                    # yes
1327-                    lease.expiration_time = new_expire_time
1328-                    f = open(self.home, 'rb+')
1329-                    self._write_lease_record(f, i, lease)
1330-                    f.close()
1331-                return
1332-        raise IndexError("unable to renew non-existent lease")
1333-
1334-    def add_or_renew_lease(self, lease_info):
1335-        try:
1336-            self.renew_lease(lease_info.renew_secret,
1337-                             lease_info.expiration_time)
1338-        except IndexError:
1339-            self.add_lease(lease_info)
1340-
1341-
1342-    def cancel_lease(self, cancel_secret):
1343-        """Remove a lease with the given cancel_secret. If the last lease is
1344-        cancelled, the file will be removed. Return the number of bytes that
1345-        were freed (by truncating the list of leases, and possibly by
1346-        deleting the file. Raise IndexError if there was no lease with the
1347-        given cancel_secret.
1348-        """
1349-
1350-        leases = list(self.get_leases())
1351-        num_leases_removed = 0
1352-        for i,lease in enumerate(leases):
1353-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1354-                leases[i] = None
1355-                num_leases_removed += 1
1356-        if not num_leases_removed:
1357-            raise IndexError("unable to find matching lease to cancel")
1358-        if num_leases_removed:
1359-            # pack and write out the remaining leases. We write these out in
1360-            # the same order as they were added, so that if we crash while
1361-            # doing this, we won't lose any non-cancelled leases.
1362-            leases = [l for l in leases if l] # remove the cancelled leases
1363-            f = open(self.home, 'rb+')
1364-            for i,lease in enumerate(leases):
1365-                self._write_lease_record(f, i, lease)
1366-            self._write_num_leases(f, len(leases))
1367-            self._truncate_leases(f, len(leases))
1368-            f.close()
1369-        space_freed = self.LEASE_SIZE * num_leases_removed
1370-        if not len(leases):
1371-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1372-            self.unlink()
1373-        return space_freed
1374-
1375-
1376 class BucketWriter(Referenceable):
1377     implements(RIBucketWriter)
1378 
1379hunk ./src/allmydata/storage/immutable.py 17
1380-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1381+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
1382         self.ss = ss
1383hunk ./src/allmydata/storage/immutable.py 19
1384-        self.incominghome = incominghome
1385-        self.finalhome = finalhome
1386-        self._max_size = max_size # don't allow the client to write more than this
1387+        self._max_size = max_size # don't allow the client to write more than this        print self.ss._active_writers.keys()
1388         self._canary = canary
1389         self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1390         self.closed = False
1391hunk ./src/allmydata/storage/immutable.py 24
1392         self.throw_out_all_data = False
1393-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1394+        self._sharefile = immutableshare
1395         # also, add our lease to the file now, so that other ones can be
1396         # added by simultaneous uploaders
1397         self._sharefile.add_lease(lease_info)
1398hunk ./src/allmydata/storage/immutable.py 45
1399         precondition(not self.closed)
1400         start = time.time()
1401 
1402-        fileutil.make_dirs(os.path.dirname(self.finalhome))
1403-        fileutil.rename(self.incominghome, self.finalhome)
1404-        try:
1405-            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1406-            # We try to delete the parent (.../ab/abcde) to avoid leaving
1407-            # these directories lying around forever, but the delete might
1408-            # fail if we're working on another share for the same storage
1409-            # index (like ab/abcde/5). The alternative approach would be to
1410-            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1411-            # ShareWriter), each of which is responsible for a single
1412-            # directory on disk, and have them use reference counting of
1413-            # their children to know when they should do the rmdir. This
1414-            # approach is simpler, but relies on os.rmdir refusing to delete
1415-            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1416-            os.rmdir(os.path.dirname(self.incominghome))
1417-            # we also delete the grandparent (prefix) directory, .../ab ,
1418-            # again to avoid leaving directories lying around. This might
1419-            # fail if there is another bucket open that shares a prefix (like
1420-            # ab/abfff).
1421-            os.rmdir(os.path.dirname(os.path.dirname(self.incominghome)))
1422-            # we leave the great-grandparent (incoming/) directory in place.
1423-        except EnvironmentError:
1424-            # ignore the "can't rmdir because the directory is not empty"
1425-            # exceptions, those are normal consequences of the
1426-            # above-mentioned conditions.
1427-            pass
1428+        self._sharefile.close()
1429+        filelen = self._sharefile.stat()
1430         self._sharefile = None
1431hunk ./src/allmydata/storage/immutable.py 48
1432+
1433         self.closed = True
1434         self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1435 
1436hunk ./src/allmydata/storage/immutable.py 52
1437-        filelen = os.stat(self.finalhome)[stat.ST_SIZE]
1438         self.ss.bucket_writer_closed(self, filelen)
1439         self.ss.add_latency("close", time.time() - start)
1440         self.ss.count("close")
1441hunk ./src/allmydata/storage/immutable.py 90
1442 class BucketReader(Referenceable):
1443     implements(RIBucketReader)
1444 
1445-    def __init__(self, ss, sharefname, storage_index=None, shnum=None):
1446+    def __init__(self, ss, share):
1447         self.ss = ss
1448hunk ./src/allmydata/storage/immutable.py 92
1449-        self._share_file = ShareFile(sharefname)
1450-        self.storage_index = storage_index
1451-        self.shnum = shnum
1452+        self._share_file = share
1453+        self.storageindex = share.storageindex
1454+        self.shnum = share.shnum
1455 
1456     def __repr__(self):
1457         return "<%s %s %s>" % (self.__class__.__name__,
1458hunk ./src/allmydata/storage/immutable.py 98
1459-                               base32.b2a_l(self.storage_index[:8], 60),
1460+                               base32.b2a_l(self.storageindex[:8], 60),
1461                                self.shnum)
1462 
1463     def remote_read(self, offset, length):
1464hunk ./src/allmydata/storage/immutable.py 110
1465 
1466     def remote_advise_corrupt_share(self, reason):
1467         return self.ss.remote_advise_corrupt_share("immutable",
1468-                                                   self.storage_index,
1469+                                                   self.storageindex,
1470                                                    self.shnum,
1471                                                    reason)
1472}
1473[creates backends/das/core.py
1474wilcoxjg@gmail.com**20110809202620
1475 Ignore-this: 2ea937f8d02aa85396135903be91ed67
1476] {
1477adddir ./src/allmydata/storage/backends/das
1478addfile ./src/allmydata/storage/backends/das/core.py
1479hunk ./src/allmydata/storage/backends/das/core.py 1
1480+import re, weakref, struct, time, stat
1481+from twisted.application import service
1482+from twisted.python.filepath import UnlistableError
1483+from twisted.python import filepath
1484+from twisted.python.filepath import FilePath
1485+from zope.interface import implements
1486+
1487+import allmydata # for __full_version__
1488+from allmydata.interfaces import IStorageBackend
1489+from allmydata.storage.backends.base import Backend
1490+from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
1491+from allmydata.util.assertutil import precondition
1492+from allmydata.interfaces import IStatsProducer, IShareStore# XXX, RIStorageServer
1493+from allmydata.util import fileutil, idlib, log, time_format
1494+from allmydata.util.fileutil import fp_make_dirs
1495+from allmydata.storage.lease import LeaseInfo
1496+from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
1497+     create_mutable_sharefile
1498+from allmydata.storage.immutable import BucketWriter, BucketReader
1499+from allmydata.storage.crawler import BucketCountingCrawler
1500+from allmydata.util.hashutil import constant_time_compare
1501+from allmydata.storage.backends.das.expirer import LeaseCheckingCrawler
1502+_pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
1503+
1504+# storage/
1505+# storage/shares/incoming
1506+#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
1507+#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
1508+# storage/shares/$START/$STORAGEINDEX
1509+# storage/shares/$START/$STORAGEINDEX/$SHARENUM
1510+
1511+# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
1512+# base-32 chars).
1513+# $SHARENUM matches this regex:
1514+NUM_RE=re.compile("^[0-9]+$")
1515+
1516+class DASCore(Backend):
1517+    implements(IStorageBackend)
1518+    def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
1519+        Backend.__init__(self)
1520+        self._setup_storage(storedir, readonly, reserved_space)
1521+        self._setup_corruption_advisory()
1522+        self._setup_bucket_counter()
1523+        self._setup_lease_checkerf(expiration_policy)
1524+
1525+    def _setup_storage(self, storedir, readonly, reserved_space):
1526+        precondition(isinstance(storedir, FilePath), storedir, FilePath) 
1527+        self.storedir = storedir
1528+        self.readonly = readonly
1529+        self.reserved_space = int(reserved_space)
1530+        self.sharedir = self.storedir.child("shares")
1531+        fileutil.fp_make_dirs(self.sharedir)
1532+        self.incomingdir = self.sharedir.child('incoming')
1533+        self._clean_incomplete()
1534+        if self.reserved_space and (self.get_available_space() is None):
1535+            log.msg("warning: [storage]reserved_space= is set, but this platform does not support an API to get disk statistics (statvfs(2) or GetDiskFreeSpaceEx), so this reservation cannot be honored",
1536+                    umid="0wZ27w", level=log.UNUSUAL)
1537+
1538+
1539+    def _clean_incomplete(self):
1540+        fileutil.fp_remove(self.incomingdir)
1541+        fileutil.fp_make_dirs(self.incomingdir)
1542+
1543+    def _setup_corruption_advisory(self):
1544+        # we don't actually create the corruption-advisory dir until necessary
1545+        self.corruption_advisory_dir = self.storedir.child("corruption-advisories")
1546+
1547+    def _setup_bucket_counter(self):
1548+        statefname = self.storedir.child("bucket_counter.state")
1549+        self.bucket_counter = BucketCountingCrawler(statefname)
1550+        self.bucket_counter.setServiceParent(self)
1551+
1552+    def _setup_lease_checkerf(self, expiration_policy):
1553+        statefile = self.storedir.child("lease_checker.state")
1554+        historyfile = self.storedir.child("lease_checker.history")
1555+        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile, expiration_policy)
1556+        self.lease_checker.setServiceParent(self)
1557+
1558+    def get_incoming_shnums(self, storageindex):
1559+        """ Return a frozenset of the shnum (as ints) of incoming shares. """
1560+        incomingthissi = si_si2dir(self.incomingdir, storageindex)
1561+        try:
1562+            childfps = [ fp for fp in incomingthissi.children() if NUM_RE.match(fp.basename()) ]
1563+            shnums = [ int(fp.basename()) for fp in childfps]
1564+            return frozenset(shnums)
1565+        except UnlistableError:
1566+            # There is no shares directory at all.
1567+            return frozenset()
1568+           
1569+    def get_shares(self, storageindex):
1570+        """ Generate ImmutableShare objects for shares we have for this
1571+        storageindex. ("Shares we have" means completed ones, excluding
1572+        incoming ones.)"""
1573+        finalstoragedir = si_si2dir(self.sharedir, storageindex)
1574+        try:
1575+            for fp in finalstoragedir.children():
1576+                fpshnumstr = fp.basename()
1577+                if NUM_RE.match(fpshnumstr):
1578+                    finalhome = finalstoragedir.child(fpshnumstr)
1579+                    yield ImmutableShare(storageindex, fpshnumstr, finalhome)
1580+        except UnlistableError:
1581+            # There is no shares directory at all.
1582+            pass
1583+       
1584+    def get_available_space(self):
1585+        if self.readonly:
1586+            return 0
1587+        return fileutil.get_available_space(self.storedir, self.reserved_space)
1588+
1589+    def make_bucket_writer(self, storageindex, shnum, max_space_per_bucket, lease_info, canary):
1590+        finalhome = si_si2dir(self.sharedir, storageindex).child(str(shnum))
1591+        incominghome = si_si2dir(self.incomingdir, storageindex).child(str(shnum))
1592+        immsh = ImmutableShare(storageindex, shnum, finalhome, incominghome, max_size=max_space_per_bucket, create=True)
1593+        bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1594+        return bw
1595+
1596+    def make_bucket_reader(self, share):
1597+        return BucketReader(self.ss, share)
1598+
1599+    def set_storage_server(self, ss):
1600+        self.ss = ss
1601+       
1602+
1603+# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1604+# and share data. The share data is accessed by RIBucketWriter.write and
1605+# RIBucketReader.read . The lease information is not accessible through these
1606+# interfaces.
1607+
1608+# The share file has the following layout:
1609+#  0x00: share file version number, four bytes, current version is 1
1610+#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1611+#  0x08: number of leases, four bytes big-endian
1612+#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1613+#  A+0x0c = B: first lease. Lease format is:
1614+#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1615+#   B+0x04: renew secret, 32 bytes (SHA256)
1616+#   B+0x24: cancel secret, 32 bytes (SHA256)
1617+#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1618+#   B+0x48: next lease, or end of record
1619+
1620+# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1621+# but it is still filled in by storage servers in case the storage server
1622+# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1623+# share file is moved from one storage server to another. The value stored in
1624+# this field is truncated, so if the actual share data length is >= 2**32,
1625+# then the value stored in this field will be the actual share data length
1626+# modulo 2**32.
1627+
1628+class ImmutableShare(object):
1629+    LEASE_SIZE = struct.calcsize(">L32s32sL")
1630+    sharetype = "immutable"
1631+
1632+    def __init__(self, storageindex, shnum, finalhome=None, incominghome=None, max_size=None, create=False):
1633+        """ If max_size is not None then I won't allow more than
1634+        max_size to be written to me. If create=True then max_size
1635+        must not be None. """
1636+        precondition((max_size is not None) or (not create), max_size, create)
1637+        self.storageindex = storageindex
1638+        self._max_size = max_size
1639+        self.incominghome = incominghome
1640+        self.finalhome = finalhome
1641+        self.shnum = shnum
1642+        if create:
1643+            # touch the file, so later callers will see that we're working on
1644+            # it. Also construct the metadata.
1645+            assert not finalhome.exists()
1646+            fp_make_dirs(self.incominghome.parent())
1647+            # The second field -- the four-byte share data length -- is no
1648+            # longer used as of Tahoe v1.3.0, but we continue to write it in
1649+            # there in case someone downgrades a storage server from >=
1650+            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1651+            # server to another, etc. We do saturation -- a share data length
1652+            # larger than 2**32-1 (what can fit into the field) is marked as
1653+            # the largest length that can fit into the field. That way, even
1654+            # if this does happen, the old < v1.3.0 server will still allow
1655+            # clients to read the first part of the share.
1656+            self.incominghome.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
1657+            self._lease_offset = max_size + 0x0c
1658+            self._num_leases = 0
1659+        else:
1660+            fh = self.finalhome.open(mode='rb')
1661+            try:
1662+                (version, unused, num_leases) = struct.unpack(">LLL", fh.read(0xc))
1663+            finally:
1664+                fh.close()
1665+            filesize = self.finalhome.getsize()
1666+            if version != 1:
1667+                msg = "sharefile %s had version %d but we wanted 1" % \
1668+                      (self.finalhome, version)
1669+                raise UnknownImmutableContainerVersionError(msg)
1670+            self._num_leases = num_leases
1671+            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1672+        self._data_offset = 0xc
1673+
1674+    def close(self):
1675+        fileutil.fp_make_dirs(self.finalhome.parent())
1676+        self.incominghome.moveTo(self.finalhome)
1677+        try:
1678+            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1679+            # We try to delete the parent (.../ab/abcde) to avoid leaving
1680+            # these directories lying around forever, but the delete might
1681+            # fail if we're working on another share for the same storage
1682+            # index (like ab/abcde/5). The alternative approach would be to
1683+            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1684+            # ShareWriter), each of which is responsible for a single
1685+            # directory on disk, and have them use reference counting of
1686+            # their children to know when they should do the rmdir. This
1687+            # approach is simpler, but relies on os.rmdir refusing to delete
1688+            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1689+            fileutil.fp_rmdir_if_empty(self.incominghome.parent())
1690+            # we also delete the grandparent (prefix) directory, .../ab ,
1691+            # again to avoid leaving directories lying around. This might
1692+            # fail if there is another bucket open that shares a prefix (like
1693+            # ab/abfff).
1694+            fileutil.fp_rmdir_if_empty(self.incominghome.parent().parent())
1695+            # we leave the great-grandparent (incoming/) directory in place.
1696+        except EnvironmentError:
1697+            # ignore the "can't rmdir because the directory is not empty"
1698+            # exceptions, those are normal consequences of the
1699+            # above-mentioned conditions.
1700+            pass
1701+        pass
1702+       
1703+    def stat(self):
1704+        return filepath.stat(self.finalhome.path)[stat.ST_SIZE]
1705+
1706+    def get_shnum(self):
1707+        return self.shnum
1708+
1709+    def unlink(self):
1710+        self.finalhome.remove()
1711+
1712+    def read_share_data(self, offset, length):
1713+        precondition(offset >= 0)
1714+        # Reads beyond the end of the data are truncated. Reads that start
1715+        # beyond the end of the data return an empty string.
1716+        seekpos = self._data_offset+offset
1717+        fsize = self.finalhome.getsize()
1718+        actuallength = max(0, min(length, fsize-seekpos))
1719+        if actuallength == 0:
1720+            return ""
1721+        fh = self.finalhome.open(mode='rb')
1722+        try:
1723+            fh.seek(seekpos)
1724+            sharedata = fh.read(actuallength)
1725+        finally:
1726+            fh.close()
1727+        return sharedata
1728+
1729+    def write_share_data(self, offset, data):
1730+        length = len(data)
1731+        precondition(offset >= 0, offset)
1732+        if self._max_size is not None and offset+length > self._max_size:
1733+            raise DataTooLargeError(self._max_size, offset, length)
1734+        fh = self.incominghome.open(mode='rb+')
1735+        try:
1736+            real_offset = self._data_offset+offset
1737+            fh.seek(real_offset)
1738+            assert fh.tell() == real_offset
1739+            fh.write(data)
1740+        finally:
1741+            fh.close()
1742+
1743+    def _write_lease_record(self, f, lease_number, lease_info):
1744+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1745+        fh = f.open()
1746+        try:
1747+            fh.seek(offset)
1748+            assert fh.tell() == offset
1749+            fh.write(lease_info.to_immutable_data())
1750+        finally:
1751+            fh.close()
1752+
1753+    def _read_num_leases(self, f):
1754+        fh = f.open() #XXX  Should be mocking FilePath.open()
1755+        try:
1756+            fh.seek(0x08)
1757+            ro = fh.read(4)
1758+            (num_leases,) = struct.unpack(">L", ro)
1759+        finally:
1760+            fh.close()
1761+        return num_leases
1762+
1763+    def _write_num_leases(self, f, num_leases):
1764+        fh = f.open()
1765+        try:
1766+            fh.seek(0x08)
1767+            fh.write(struct.pack(">L", num_leases))
1768+        finally:
1769+            fh.close()
1770+
1771+    def _truncate_leases(self, f, num_leases):
1772+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1773+
1774+    def get_leases(self):
1775+        """Yields a LeaseInfo instance for all leases."""
1776+        fh = self.finalhome.open(mode='rb')
1777+        (version, unused, num_leases) = struct.unpack(">LLL", fh.read(0xc))
1778+        fh.seek(self._lease_offset)
1779+        for i in range(num_leases):
1780+            data = fh.read(self.LEASE_SIZE)
1781+            if data:
1782+                yield LeaseInfo().from_immutable_data(data)
1783+
1784+    def add_lease(self, lease_info):
1785+        num_leases = self._read_num_leases(self.incominghome)
1786+        self._write_lease_record(self.incominghome, num_leases, lease_info)
1787+        self._write_num_leases(self.incominghome, num_leases+1)
1788+       
1789+    def renew_lease(self, renew_secret, new_expire_time):
1790+        for i,lease in enumerate(self.get_leases()):
1791+            if constant_time_compare(lease.renew_secret, renew_secret):
1792+                # yup. See if we need to update the owner time.
1793+                if new_expire_time > lease.expiration_time:
1794+                    # yes
1795+                    lease.expiration_time = new_expire_time
1796+                    f = open(self.finalhome, 'rb+')
1797+                    self._write_lease_record(f, i, lease)
1798+                    f.close()
1799+                return
1800+        raise IndexError("unable to renew non-existent lease")
1801+
1802+    def add_or_renew_lease(self, lease_info):
1803+        try:
1804+            self.renew_lease(lease_info.renew_secret,
1805+                             lease_info.expiration_time)
1806+        except IndexError:
1807+            self.add_lease(lease_info)
1808+
1809+    def cancel_lease(self, cancel_secret):
1810+        """Remove a lease with the given cancel_secret. If the last lease is
1811+        cancelled, the file will be removed. Return the number of bytes that
1812+        were freed (by truncating the list of leases, and possibly by
1813+        deleting the file. Raise IndexError if there was no lease with the
1814+        given cancel_secret.
1815+        """
1816+
1817+        leases = list(self.get_leases())
1818+        num_leases_removed = 0
1819+        for i,lease in enumerate(leases):
1820+            if constant_time_compare(lease.cancel_secret, cancel_secret):
1821+                leases[i] = None
1822+                num_leases_removed += 1
1823+        if not num_leases_removed:
1824+            raise IndexError("unable to find matching lease to cancel")
1825+        if num_leases_removed:
1826+            # pack and write out the remaining leases. We write these out in
1827+            # the same order as they were added, so that if we crash while
1828+            # doing this, we won't lose any non-cancelled leases.
1829+            leases = [l for l in leases if l] # remove the cancelled leases
1830+            f = open(self.finalhome, 'rb+')
1831+            for i,lease in enumerate(leases):
1832+                self._write_lease_record(f, i, lease)
1833+            self._write_num_leases(f, len(leases))
1834+            self._truncate_leases(f, len(leases))
1835+            f.close()
1836+        space_freed = self.LEASE_SIZE * num_leases_removed
1837+        if not len(leases):
1838+            space_freed += os.stat(self.finalhome)[stat.ST_SIZE]
1839+            self.unlink()
1840+        return space_freed
1841}
1842[change backends/das/core.py to correct which interfaces are implemented
1843wilcoxjg@gmail.com**20110809203123
1844 Ignore-this: 7f9331a04b55f7feee4335abee011e14
1845] hunk ./src/allmydata/storage/backends/das/core.py 13
1846 from allmydata.storage.backends.base import Backend
1847 from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
1848 from allmydata.util.assertutil import precondition
1849-from allmydata.interfaces import IStatsProducer, IShareStore# XXX, RIStorageServer
1850+from allmydata.interfaces import IStorageBackend
1851 from allmydata.util import fileutil, idlib, log, time_format
1852 from allmydata.util.fileutil import fp_make_dirs
1853 from allmydata.storage.lease import LeaseInfo
1854[util/fileutil.py now expects and manipulates twisted.python.filepath.FilePath objects
1855wilcoxjg@gmail.com**20110809203321
1856 Ignore-this: 12c8aa13424ed51a5df09b92a454627
1857] {
1858hunk ./src/allmydata/util/fileutil.py 5
1859 Futz with files like a pro.
1860 """
1861 
1862-import sys, exceptions, os, stat, tempfile, time, binascii
1863+import errno, sys, exceptions, os, stat, tempfile, time, binascii
1864+
1865+from allmydata.util.assertutil import precondition
1866 
1867 from twisted.python import log
1868hunk ./src/allmydata/util/fileutil.py 10
1869+from twisted.python.filepath import FilePath, UnlistableError
1870 
1871 from pycryptopp.cipher.aes import AES
1872 
1873hunk ./src/allmydata/util/fileutil.py 189
1874             raise tx
1875         raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirname # careful not to construct an IOError with a 2-tuple, as that has a special meaning...
1876 
1877-def rm_dir(dirname):
1878+def fp_make_dirs(dirfp):
1879+    """
1880+    An idempotent version of FilePath.makedirs().  If the dir already
1881+    exists, do nothing and return without raising an exception.  If this
1882+    call creates the dir, return without raising an exception.  If there is
1883+    an error that prevents creation or if the directory gets deleted after
1884+    fp_make_dirs() creates it and before fp_make_dirs() checks that it
1885+    exists, raise an exception.
1886+    """
1887+    log.msg( "xxx 0 %s" % (dirfp,))
1888+    tx = None
1889+    try:
1890+        dirfp.makedirs()
1891+    except OSError, x:
1892+        tx = x
1893+
1894+    if not dirfp.isdir():
1895+        if tx:
1896+            raise tx
1897+        raise exceptions.IOError, "unknown error prevented creation of directory, or deleted the directory immediately after creation: %s" % dirfp # careful not to construct an IOError with a 2-tuple, as that has a special meaning...
1898+
1899+def fp_rmdir_if_empty(dirfp):
1900+    """ Remove the directory if it is empty. """
1901+    try:
1902+        os.rmdir(dirfp.path)
1903+    except OSError, e:
1904+        if e.errno != errno.ENOTEMPTY:
1905+            raise
1906+    else:
1907+        dirfp.changed()
1908+
1909+def rmtree(dirname):
1910     """
1911     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
1912     already gone, do nothing and return without raising an exception.  If this
1913hunk ./src/allmydata/util/fileutil.py 239
1914             else:
1915                 remove(fullname)
1916         os.rmdir(dirname)
1917-    except Exception, le:
1918-        # Ignore "No such file or directory"
1919-        if (not isinstance(le, OSError)) or le.args[0] != 2:
1920+    except EnvironmentError, le:
1921+        # Ignore "No such file or directory", collect any other exception.
1922+        if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT):
1923             excs.append(le)
1924hunk ./src/allmydata/util/fileutil.py 243
1925+    except Exception, le:
1926+        excs.append(le)
1927 
1928     # Okay, now we've recursively removed everything, ignoring any "No
1929     # such file or directory" errors, and collecting any other errors.
1930hunk ./src/allmydata/util/fileutil.py 256
1931             raise OSError, "Failed to remove dir for unknown reason."
1932         raise OSError, excs
1933 
1934+def fp_remove(dirfp):
1935+    """
1936+    An idempotent version of shutil.rmtree().  If the dir is already gone,
1937+    do nothing and return without raising an exception.  If this call
1938+    removes the dir, return without raising an exception.  If there is an
1939+    error that prevents removal or if the directory gets created again by
1940+    someone else after this deletes it and before this checks that it is
1941+    gone, raise an exception.
1942+    """
1943+    try:
1944+        dirfp.remove()
1945+    except UnlistableError, e:
1946+        if e.originalException.errno != errno.ENOENT:
1947+            raise
1948+    except OSError, e:
1949+        if e.errno != errno.ENOENT:
1950+            raise
1951+
1952+def rm_dir(dirname):
1953+    # Renamed to be like shutil.rmtree and unlike rmdir.
1954+    return rmtree(dirname)
1955 
1956 def remove_if_possible(f):
1957     try:
1958hunk ./src/allmydata/util/fileutil.py 387
1959         import traceback
1960         traceback.print_exc()
1961 
1962-def get_disk_stats(whichdir, reserved_space=0):
1963+def get_disk_stats(whichdirfp, reserved_space=0):
1964     """Return disk statistics for the storage disk, in the form of a dict
1965     with the following fields.
1966       total:            total bytes on disk
1967hunk ./src/allmydata/util/fileutil.py 408
1968     you can pass how many bytes you would like to leave unused on this
1969     filesystem as reserved_space.
1970     """
1971+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
1972 
1973     if have_GetDiskFreeSpaceExW:
1974         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
1975hunk ./src/allmydata/util/fileutil.py 419
1976         n_free_for_nonroot = c_ulonglong(0)
1977         n_total            = c_ulonglong(0)
1978         n_free_for_root    = c_ulonglong(0)
1979-        retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
1980+        retval = GetDiskFreeSpaceExW(whichdirfp.path, byref(n_free_for_nonroot),
1981                                                byref(n_total),
1982                                                byref(n_free_for_root))
1983         if retval == 0:
1984hunk ./src/allmydata/util/fileutil.py 424
1985             raise OSError("Windows error %d attempting to get disk statistics for %r"
1986-                          % (GetLastError(), whichdir))
1987+                          % (GetLastError(), whichdirfp.path))
1988         free_for_nonroot = n_free_for_nonroot.value
1989         total            = n_total.value
1990         free_for_root    = n_free_for_root.value
1991hunk ./src/allmydata/util/fileutil.py 433
1992         # <http://docs.python.org/library/os.html#os.statvfs>
1993         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
1994         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
1995-        s = os.statvfs(whichdir)
1996+        s = os.statvfs(whichdirfp.path)
1997 
1998         # on my mac laptop:
1999         #  statvfs(2) is a wrapper around statfs(2).
2000hunk ./src/allmydata/util/fileutil.py 460
2001              'avail': avail,
2002            }
2003 
2004-def get_available_space(whichdir, reserved_space):
2005+def get_available_space(whichdirfp, reserved_space):
2006     """Returns available space for share storage in bytes, or None if no
2007     API to get this information is available.
2008 
2009hunk ./src/allmydata/util/fileutil.py 472
2010     you can pass how many bytes you would like to leave unused on this
2011     filesystem as reserved_space.
2012     """
2013+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
2014     try:
2015hunk ./src/allmydata/util/fileutil.py 474
2016-        return get_disk_stats(whichdir, reserved_space)['avail']
2017+        return get_disk_stats(whichdirfp, reserved_space)['avail']
2018     except AttributeError:
2019         return None
2020hunk ./src/allmydata/util/fileutil.py 477
2021-    except EnvironmentError:
2022-        log.msg("OS call to get disk statistics failed")
2023-        return 0
2024}
2025[add expirer.py
2026wilcoxjg@gmail.com**20110809203519
2027 Ignore-this: b09d460593f0e0aa065e867d5159455b
2028] {
2029addfile ./src/allmydata/storage/backends/das/expirer.py
2030hunk ./src/allmydata/storage/backends/das/expirer.py 1
2031+import time, os, pickle, struct # os, pickle, and struct will almost certainly be migrated to the backend...
2032+from allmydata.storage.crawler import ShareCrawler
2033+from allmydata.storage.common import UnknownMutableContainerVersionError, \
2034+     UnknownImmutableContainerVersionError
2035+from twisted.python import log as twlog
2036+
2037+class LeaseCheckingCrawler(ShareCrawler):
2038+    """I examine the leases on all shares, determining which are still valid
2039+    and which have expired. I can remove the expired leases (if so
2040+    configured), and the share will be deleted when the last lease is
2041+    removed.
2042+
2043+    I collect statistics on the leases and make these available to a web
2044+    status page, including:
2045+
2046+    Space recovered during this cycle-so-far:
2047+     actual (only if expiration_enabled=True):
2048+      num-buckets, num-shares, sum of share sizes, real disk usage
2049+      ('real disk usage' means we use stat(fn).st_blocks*512 and include any
2050+       space used by the directory)
2051+     what it would have been with the original lease expiration time
2052+     what it would have been with our configured expiration time
2053+
2054+    Prediction of space that will be recovered during the rest of this cycle
2055+    Prediction of space that will be recovered by the entire current cycle.
2056+
2057+    Space recovered during the last 10 cycles  <-- saved in separate pickle
2058+
2059+    Shares/buckets examined:
2060+     this cycle-so-far
2061+     prediction of rest of cycle
2062+     during last 10 cycles <-- separate pickle
2063+    start/finish time of last 10 cycles  <-- separate pickle
2064+    expiration time used for last 10 cycles <-- separate pickle
2065+
2066+    Histogram of leases-per-share:
2067+     this-cycle-to-date
2068+     last 10 cycles <-- separate pickle
2069+    Histogram of lease ages, buckets = 1day
2070+     cycle-to-date
2071+     last 10 cycles <-- separate pickle
2072+
2073+    All cycle-to-date values remain valid until the start of the next cycle.
2074+
2075+    """
2076+
2077+    slow_start = 360 # wait 6 minutes after startup
2078+    minimum_cycle_time = 12*60*60 # not more than twice per day
2079+
2080+    def __init__(self, statefile, historyfp, expiration_policy):
2081+        self.historyfp = historyfp
2082+        self.expiration_enabled = expiration_policy['enabled']
2083+        self.mode = expiration_policy['mode']
2084+        self.override_lease_duration = None
2085+        self.cutoff_date = None
2086+        if self.mode == "age":
2087+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
2088+            self.override_lease_duration = expiration_policy['override_lease_duration']# seconds
2089+        elif self.mode == "cutoff-date":
2090+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
2091+            assert cutoff_date is not None
2092+            self.cutoff_date = expiration_policy['cutoff_date']
2093+        else:
2094+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
2095+        self.sharetypes_to_expire = expiration_policy['sharetypes']
2096+        ShareCrawler.__init__(self, statefile)
2097+
2098+    def add_initial_state(self):
2099+        # we fill ["cycle-to-date"] here (even though they will be reset in
2100+        # self.started_cycle) just in case someone grabs our state before we
2101+        # get started: unit tests do this
2102+        so_far = self.create_empty_cycle_dict()
2103+        self.state.setdefault("cycle-to-date", so_far)
2104+        # in case we upgrade the code while a cycle is in progress, update
2105+        # the keys individually
2106+        for k in so_far:
2107+            self.state["cycle-to-date"].setdefault(k, so_far[k])
2108+
2109+        # initialize history
2110+        if not self.historyfp.exists():
2111+            history = {} # cyclenum -> dict
2112+            self.historyfp.setContent(pickle.dumps(history))
2113+
2114+    def create_empty_cycle_dict(self):
2115+        recovered = self.create_empty_recovered_dict()
2116+        so_far = {"corrupt-shares": [],
2117+                  "space-recovered": recovered,
2118+                  "lease-age-histogram": {}, # (minage,maxage)->count
2119+                  "leases-per-share-histogram": {}, # leasecount->numshares
2120+                  }
2121+        return so_far
2122+
2123+    def create_empty_recovered_dict(self):
2124+        recovered = {}
2125+        for a in ("actual", "original", "configured", "examined"):
2126+            for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2127+                recovered[a+"-"+b] = 0
2128+                recovered[a+"-"+b+"-mutable"] = 0
2129+                recovered[a+"-"+b+"-immutable"] = 0
2130+        return recovered
2131+
2132+    def started_cycle(self, cycle):
2133+        self.state["cycle-to-date"] = self.create_empty_cycle_dict()
2134+
2135+    def stat(self, fn):
2136+        return os.stat(fn)
2137+
2138+    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
2139+        bucketdir = os.path.join(prefixdir, storage_index_b32)
2140+        s = self.stat(bucketdir)
2141+        would_keep_shares = []
2142+        wks = None
2143+
2144+        for fn in os.listdir(bucketdir):
2145+            try:
2146+                shnum = int(fn)
2147+            except ValueError:
2148+                continue # non-numeric means not a sharefile
2149+            sharefile = os.path.join(bucketdir, fn)
2150+            try:
2151+                wks = self.process_share(sharefile)
2152+            except (UnknownMutableContainerVersionError,
2153+                    UnknownImmutableContainerVersionError,
2154+                    struct.error):
2155+                twlog.msg("lease-checker error processing %s" % sharefile)
2156+                twlog.err()
2157+                which = (storage_index_b32, shnum)
2158+                self.state["cycle-to-date"]["corrupt-shares"].append(which)
2159+                wks = (1, 1, 1, "unknown")
2160+            would_keep_shares.append(wks)
2161+
2162+        sharetype = None
2163+        if wks:
2164+            # use the last share's sharetype as the buckettype
2165+            sharetype = wks[3]
2166+        rec = self.state["cycle-to-date"]["space-recovered"]
2167+        self.increment(rec, "examined-buckets", 1)
2168+        if sharetype:
2169+            self.increment(rec, "examined-buckets-"+sharetype, 1)
2170+
2171+        try:
2172+            bucket_diskbytes = s.st_blocks * 512
2173+        except AttributeError:
2174+            bucket_diskbytes = 0 # no stat().st_blocks on windows
2175+        if sum([wks[0] for wks in would_keep_shares]) == 0:
2176+            self.increment_bucketspace("original", bucket_diskbytes, sharetype)
2177+        if sum([wks[1] for wks in would_keep_shares]) == 0:
2178+            self.increment_bucketspace("configured", bucket_diskbytes, sharetype)
2179+        if sum([wks[2] for wks in would_keep_shares]) == 0:
2180+            self.increment_bucketspace("actual", bucket_diskbytes, sharetype)
2181+
2182+    def process_share(self, sharefilename):
2183+        # first, find out what kind of a share it is
2184+        f = open(sharefilename, "rb")
2185+        prefix = f.read(32)
2186+        f.close()
2187+        if prefix == MutableShareFile.MAGIC:
2188+            sf = MutableShareFile(sharefilename)
2189+        else:
2190+            # otherwise assume it's immutable
2191+            sf = FSBShare(sharefilename)
2192+        sharetype = sf.sharetype
2193+        now = time.time()
2194+        s = self.stat(sharefilename)
2195+
2196+        num_leases = 0
2197+        num_valid_leases_original = 0
2198+        num_valid_leases_configured = 0
2199+        expired_leases_configured = []
2200+
2201+        for li in sf.get_leases():
2202+            num_leases += 1
2203+            original_expiration_time = li.get_expiration_time()
2204+            grant_renew_time = li.get_grant_renew_time_time()
2205+            age = li.get_age()
2206+            self.add_lease_age_to_histogram(age)
2207+
2208+            #  expired-or-not according to original expiration time
2209+            if original_expiration_time > now:
2210+                num_valid_leases_original += 1
2211+
2212+            #  expired-or-not according to our configured age limit
2213+            expired = False
2214+            if self.mode == "age":
2215+                age_limit = original_expiration_time
2216+                if self.override_lease_duration is not None:
2217+                    age_limit = self.override_lease_duration
2218+                if age > age_limit:
2219+                    expired = True
2220+            else:
2221+                assert self.mode == "cutoff-date"
2222+                if grant_renew_time < self.cutoff_date:
2223+                    expired = True
2224+            if sharetype not in self.sharetypes_to_expire:
2225+                expired = False
2226+
2227+            if expired:
2228+                expired_leases_configured.append(li)
2229+            else:
2230+                num_valid_leases_configured += 1
2231+
2232+        so_far = self.state["cycle-to-date"]
2233+        self.increment(so_far["leases-per-share-histogram"], num_leases, 1)
2234+        self.increment_space("examined", s, sharetype)
2235+
2236+        would_keep_share = [1, 1, 1, sharetype]
2237+
2238+        if self.expiration_enabled:
2239+            for li in expired_leases_configured:
2240+                sf.cancel_lease(li.cancel_secret)
2241+
2242+        if num_valid_leases_original == 0:
2243+            would_keep_share[0] = 0
2244+            self.increment_space("original", s, sharetype)
2245+
2246+        if num_valid_leases_configured == 0:
2247+            would_keep_share[1] = 0
2248+            self.increment_space("configured", s, sharetype)
2249+            if self.expiration_enabled:
2250+                would_keep_share[2] = 0
2251+                self.increment_space("actual", s, sharetype)
2252+
2253+        return would_keep_share
2254+
2255+    def increment_space(self, a, s, sharetype):
2256+        sharebytes = s.st_size
2257+        try:
2258+            # note that stat(2) says that st_blocks is 512 bytes, and that
2259+            # st_blksize is "optimal file sys I/O ops blocksize", which is
2260+            # independent of the block-size that st_blocks uses.
2261+            diskbytes = s.st_blocks * 512
2262+        except AttributeError:
2263+            # the docs say that st_blocks is only on linux. I also see it on
2264+            # MacOS. But it isn't available on windows.
2265+            diskbytes = sharebytes
2266+        so_far_sr = self.state["cycle-to-date"]["space-recovered"]
2267+        self.increment(so_far_sr, a+"-shares", 1)
2268+        self.increment(so_far_sr, a+"-sharebytes", sharebytes)
2269+        self.increment(so_far_sr, a+"-diskbytes", diskbytes)
2270+        if sharetype:
2271+            self.increment(so_far_sr, a+"-shares-"+sharetype, 1)
2272+            self.increment(so_far_sr, a+"-sharebytes-"+sharetype, sharebytes)
2273+            self.increment(so_far_sr, a+"-diskbytes-"+sharetype, diskbytes)
2274+
2275+    def increment_bucketspace(self, a, bucket_diskbytes, sharetype):
2276+        rec = self.state["cycle-to-date"]["space-recovered"]
2277+        self.increment(rec, a+"-diskbytes", bucket_diskbytes)
2278+        self.increment(rec, a+"-buckets", 1)
2279+        if sharetype:
2280+            self.increment(rec, a+"-diskbytes-"+sharetype, bucket_diskbytes)
2281+            self.increment(rec, a+"-buckets-"+sharetype, 1)
2282+
2283+    def increment(self, d, k, delta=1):
2284+        if k not in d:
2285+            d[k] = 0
2286+        d[k] += delta
2287+
2288+    def add_lease_age_to_histogram(self, age):
2289+        bucket_interval = 24*60*60
2290+        bucket_number = int(age/bucket_interval)
2291+        bucket_start = bucket_number * bucket_interval
2292+        bucket_end = bucket_start + bucket_interval
2293+        k = (bucket_start, bucket_end)
2294+        self.increment(self.state["cycle-to-date"]["lease-age-histogram"], k, 1)
2295+
2296+    def convert_lease_age_histogram(self, lah):
2297+        # convert { (minage,maxage) : count } into [ (minage,maxage,count) ]
2298+        # since the former is not JSON-safe (JSON dictionaries must have
2299+        # string keys).
2300+        json_safe_lah = []
2301+        for k in sorted(lah):
2302+            (minage,maxage) = k
2303+            json_safe_lah.append( (minage, maxage, lah[k]) )
2304+        return json_safe_lah
2305+
2306+    def finished_cycle(self, cycle):
2307+        # add to our history state, prune old history
2308+        h = {}
2309+
2310+        start = self.state["current-cycle-start-time"]
2311+        now = time.time()
2312+        h["cycle-start-finish-times"] = (start, now)
2313+        h["expiration-enabled"] = self.expiration_enabled
2314+        h["configured-expiration-mode"] = (self.mode,
2315+                                           self.override_lease_duration,
2316+                                           self.cutoff_date,
2317+                                           self.sharetypes_to_expire)
2318+
2319+        s = self.state["cycle-to-date"]
2320+
2321+        # state["lease-age-histogram"] is a dictionary (mapping
2322+        # (minage,maxage) tuple to a sharecount), but we report
2323+        # self.get_state()["lease-age-histogram"] as a list of
2324+        # (min,max,sharecount) tuples, because JSON can handle that better.
2325+        # We record the list-of-tuples form into the history for the same
2326+        # reason.
2327+        lah = self.convert_lease_age_histogram(s["lease-age-histogram"])
2328+        h["lease-age-histogram"] = lah
2329+        h["leases-per-share-histogram"] = s["leases-per-share-histogram"].copy()
2330+        h["corrupt-shares"] = s["corrupt-shares"][:]
2331+        # note: if ["shares-recovered"] ever acquires an internal dict, this
2332+        # copy() needs to become a deepcopy
2333+        h["space-recovered"] = s["space-recovered"].copy()
2334+
2335+        history = pickle.load(self.historyfp.getContent())
2336+        history[cycle] = h
2337+        while len(history) > 10:
2338+            oldcycles = sorted(history.keys())
2339+            del history[oldcycles[0]]
2340+        self.historyfp.setContent(pickle.dumps(history))
2341+
2342+    def get_state(self):
2343+        """In addition to the crawler state described in
2344+        ShareCrawler.get_state(), I return the following keys which are
2345+        specific to the lease-checker/expirer. Note that the non-history keys
2346+        (with 'cycle' in their names) are only present if a cycle is
2347+        currently running. If the crawler is between cycles, it appropriate
2348+        to show the latest item in the 'history' key instead. Also note that
2349+        each history item has all the data in the 'cycle-to-date' value, plus
2350+        cycle-start-finish-times.
2351+
2352+         cycle-to-date:
2353+          expiration-enabled
2354+          configured-expiration-mode
2355+          lease-age-histogram (list of (minage,maxage,sharecount) tuples)
2356+          leases-per-share-histogram
2357+          corrupt-shares (list of (si_b32,shnum) tuples, minimal verification)
2358+          space-recovered
2359+
2360+         estimated-remaining-cycle:
2361+          # Values may be None if not enough data has been gathered to
2362+          # produce an estimate.
2363+          space-recovered
2364+
2365+         estimated-current-cycle:
2366+          # cycle-to-date plus estimated-remaining. Values may be None if
2367+          # not enough data has been gathered to produce an estimate.
2368+          space-recovered
2369+
2370+         history: maps cyclenum to a dict with the following keys:
2371+          cycle-start-finish-times
2372+          expiration-enabled
2373+          configured-expiration-mode
2374+          lease-age-histogram
2375+          leases-per-share-histogram
2376+          corrupt-shares
2377+          space-recovered
2378+
2379+         The 'space-recovered' structure is a dictionary with the following
2380+         keys:
2381+          # 'examined' is what was looked at
2382+          examined-buckets, examined-buckets-mutable, examined-buckets-immutable
2383+          examined-shares, -mutable, -immutable
2384+          examined-sharebytes, -mutable, -immutable
2385+          examined-diskbytes, -mutable, -immutable
2386+
2387+          # 'actual' is what was actually deleted
2388+          actual-buckets, -mutable, -immutable
2389+          actual-shares, -mutable, -immutable
2390+          actual-sharebytes, -mutable, -immutable
2391+          actual-diskbytes, -mutable, -immutable
2392+
2393+          # would have been deleted, if the original lease timer was used
2394+          original-buckets, -mutable, -immutable
2395+          original-shares, -mutable, -immutable
2396+          original-sharebytes, -mutable, -immutable
2397+          original-diskbytes, -mutable, -immutable
2398+
2399+          # would have been deleted, if our configured max_age was used
2400+          configured-buckets, -mutable, -immutable
2401+          configured-shares, -mutable, -immutable
2402+          configured-sharebytes, -mutable, -immutable
2403+          configured-diskbytes, -mutable, -immutable
2404+
2405+        """
2406+        progress = self.get_progress()
2407+
2408+        state = ShareCrawler.get_state(self) # does a shallow copy
2409+        history = pickle.load(self.historyfp.getContent())
2410+        state["history"] = history
2411+
2412+        if not progress["cycle-in-progress"]:
2413+            del state["cycle-to-date"]
2414+            return state
2415+
2416+        so_far = state["cycle-to-date"].copy()
2417+        state["cycle-to-date"] = so_far
2418+
2419+        lah = so_far["lease-age-histogram"]
2420+        so_far["lease-age-histogram"] = self.convert_lease_age_histogram(lah)
2421+        so_far["expiration-enabled"] = self.expiration_enabled
2422+        so_far["configured-expiration-mode"] = (self.mode,
2423+                                                self.override_lease_duration,
2424+                                                self.cutoff_date,
2425+                                                self.sharetypes_to_expire)
2426+
2427+        so_far_sr = so_far["space-recovered"]
2428+        remaining_sr = {}
2429+        remaining = {"space-recovered": remaining_sr}
2430+        cycle_sr = {}
2431+        cycle = {"space-recovered": cycle_sr}
2432+
2433+        if progress["cycle-complete-percentage"] > 0.0:
2434+            pc = progress["cycle-complete-percentage"] / 100.0
2435+            m = (1-pc)/pc
2436+            for a in ("actual", "original", "configured", "examined"):
2437+                for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2438+                    for c in ("", "-mutable", "-immutable"):
2439+                        k = a+"-"+b+c
2440+                        remaining_sr[k] = m * so_far_sr[k]
2441+                        cycle_sr[k] = so_far_sr[k] + remaining_sr[k]
2442+        else:
2443+            for a in ("actual", "original", "configured", "examined"):
2444+                for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2445+                    for c in ("", "-mutable", "-immutable"):
2446+                        k = a+"-"+b+c
2447+                        remaining_sr[k] = None
2448+                        cycle_sr[k] = None
2449+
2450+        state["estimated-remaining-cycle"] = remaining
2451+        state["estimated-current-cycle"] = cycle
2452+        return state
2453}
2454[Changes I have made that aren't necessary for the test_backends.py suite to pass.
2455wilcoxjg@gmail.com**20110809203811
2456 Ignore-this: 117d49047456013f382ffc0559f00c40
2457] {
2458hunk ./src/allmydata/storage/crawler.py 1
2459-
2460 import os, time, struct
2461 import cPickle as pickle
2462 from twisted.internet import reactor
2463hunk ./src/allmydata/storage/crawler.py 6
2464 from twisted.application import service
2465 from allmydata.storage.common import si_b2a
2466-from allmydata.util import fileutil
2467 
2468 class TimeSliceExceeded(Exception):
2469     pass
2470hunk ./src/allmydata/storage/crawler.py 11
2471 
2472 class ShareCrawler(service.MultiService):
2473-    """A ShareCrawler subclass is attached to a StorageServer, and
2474+    """A subclass of ShareCrawler is attached to a StorageServer, and
2475     periodically walks all of its shares, processing each one in some
2476     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
2477     since large servers can easily have a terabyte of shares, in several
2478hunk ./src/allmydata/storage/crawler.py 29
2479     We assume that the normal upload/download/get_buckets traffic of a tahoe
2480     grid will cause the prefixdir contents to be mostly cached in the kernel,
2481     or that the number of buckets in each prefixdir will be small enough to
2482-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
2483+    load quickly. A 1TB allmydata.com server was measured to have 2.56 * 10^6
2484     buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
2485     prefix. On this server, each prefixdir took 130ms-200ms to list the first
2486     time, and 17ms to list the second time.
2487hunk ./src/allmydata/storage/crawler.py 66
2488     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
2489     minimum_cycle_time = 300 # don't run a cycle faster than this
2490 
2491-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
2492+    def __init__(self, statefp, allowed_cpu_percentage=None):
2493         service.MultiService.__init__(self)
2494         if allowed_cpu_percentage is not None:
2495             self.allowed_cpu_percentage = allowed_cpu_percentage
2496hunk ./src/allmydata/storage/crawler.py 70
2497-        self.server = server
2498-        self.sharedir = server.sharedir
2499-        self.statefile = statefile
2500+        self.statefp = statefp
2501         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
2502                          for i in range(2**10)]
2503         self.prefixes.sort()
2504hunk ./src/allmydata/storage/crawler.py 190
2505         #                            of the last bucket to be processed, or
2506         #                            None if we are sleeping between cycles
2507         try:
2508-            f = open(self.statefile, "rb")
2509-            state = pickle.load(f)
2510-            f.close()
2511+            state = pickle.loads(self.statefp.getContent())
2512         except EnvironmentError:
2513             state = {"version": 1,
2514                      "last-cycle-finished": None,
2515hunk ./src/allmydata/storage/crawler.py 226
2516         else:
2517             last_complete_prefix = self.prefixes[lcpi]
2518         self.state["last-complete-prefix"] = last_complete_prefix
2519-        tmpfile = self.statefile + ".tmp"
2520-        f = open(tmpfile, "wb")
2521-        pickle.dump(self.state, f)
2522-        f.close()
2523-        fileutil.move_into_place(tmpfile, self.statefile)
2524+        self.statefp.setContent(pickle.dumps(self.state))
2525 
2526     def startService(self):
2527         # arrange things to look like we were just sleeping, so
2528hunk ./src/allmydata/storage/crawler.py 438
2529 
2530     minimum_cycle_time = 60*60 # we don't need this more than once an hour
2531 
2532-    def __init__(self, server, statefile, num_sample_prefixes=1):
2533-        ShareCrawler.__init__(self, server, statefile)
2534+    def __init__(self, statefp, num_sample_prefixes=1):
2535+        ShareCrawler.__init__(self, statefp)
2536         self.num_sample_prefixes = num_sample_prefixes
2537 
2538     def add_initial_state(self):
2539hunk ./src/allmydata/storage/crawler.py 478
2540             old_cycle,buckets = self.state["storage-index-samples"][prefix]
2541             if old_cycle != cycle:
2542                 del self.state["storage-index-samples"][prefix]
2543-
2544hunk ./src/allmydata/storage/lease.py 17
2545 
2546     def get_expiration_time(self):
2547         return self.expiration_time
2548+
2549     def get_grant_renew_time_time(self):
2550         # hack, based upon fixed 31day expiration period
2551         return self.expiration_time - 31*24*60*60
2552hunk ./src/allmydata/storage/lease.py 21
2553+
2554     def get_age(self):
2555         return time.time() - self.get_grant_renew_time_time()
2556 
2557hunk ./src/allmydata/storage/lease.py 32
2558          self.expiration_time) = struct.unpack(">L32s32sL", data)
2559         self.nodeid = None
2560         return self
2561+
2562     def to_immutable_data(self):
2563         return struct.pack(">L32s32sL",
2564                            self.owner_num,
2565hunk ./src/allmydata/storage/lease.py 45
2566                            int(self.expiration_time),
2567                            self.renew_secret, self.cancel_secret,
2568                            self.nodeid)
2569+
2570     def from_mutable_data(self, data):
2571         (self.owner_num,
2572          self.expiration_time,
2573}
2574[add __init__.py to backend and core and null
2575wilcoxjg@gmail.com**20110810033751
2576 Ignore-this: 1c72bc54951033ab433c38de58bdc39c
2577] {
2578addfile ./src/allmydata/storage/backends/__init__.py
2579addfile ./src/allmydata/storage/backends/null/__init__.py
2580}
2581[whitespace-cleanup
2582wilcoxjg@gmail.com**20110810170847
2583 Ignore-this: 7a278e7c87c6fcd2e5ed783667c8b746
2584] {
2585hunk ./src/allmydata/interfaces.py 1
2586-
2587 from zope.interface import Interface
2588 from foolscap.api import StringConstraint, ListOf, TupleOf, SetOf, DictOf, \
2589      ChoiceOf, IntegerConstraint, Any, RemoteInterface, Referenceable
2590hunk ./src/allmydata/storage/backends/das/core.py 47
2591         self._setup_lease_checkerf(expiration_policy)
2592 
2593     def _setup_storage(self, storedir, readonly, reserved_space):
2594-        precondition(isinstance(storedir, FilePath), storedir, FilePath) 
2595+        precondition(isinstance(storedir, FilePath), storedir, FilePath)
2596         self.storedir = storedir
2597         self.readonly = readonly
2598         self.reserved_space = int(reserved_space)
2599hunk ./src/allmydata/storage/backends/das/core.py 89
2600         except UnlistableError:
2601             # There is no shares directory at all.
2602             return frozenset()
2603-           
2604+
2605     def get_shares(self, storageindex):
2606         """ Generate ImmutableShare objects for shares we have for this
2607         storageindex. ("Shares we have" means completed ones, excluding
2608hunk ./src/allmydata/storage/backends/das/core.py 104
2609         except UnlistableError:
2610             # There is no shares directory at all.
2611             pass
2612-       
2613+
2614     def get_available_space(self):
2615         if self.readonly:
2616             return 0
2617hunk ./src/allmydata/storage/backends/das/core.py 122
2618 
2619     def set_storage_server(self, ss):
2620         self.ss = ss
2621-       
2622+
2623 
2624 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
2625 # and share data. The share data is accessed by RIBucketWriter.write and
2626hunk ./src/allmydata/storage/backends/das/core.py 223
2627             # above-mentioned conditions.
2628             pass
2629         pass
2630-       
2631+
2632     def stat(self):
2633         return filepath.stat(self.finalhome.path)[stat.ST_SIZE]
2634 
2635hunk ./src/allmydata/storage/backends/das/core.py 309
2636         num_leases = self._read_num_leases(self.incominghome)
2637         self._write_lease_record(self.incominghome, num_leases, lease_info)
2638         self._write_num_leases(self.incominghome, num_leases+1)
2639-       
2640+
2641     def renew_lease(self, renew_secret, new_expire_time):
2642         for i,lease in enumerate(self.get_leases()):
2643             if constant_time_compare(lease.renew_secret, renew_secret):
2644hunk ./src/allmydata/storage/common.py 1
2645-
2646 import os.path
2647 from allmydata.util import base32
2648 
2649hunk ./src/allmydata/storage/server.py 149
2650 
2651         if self.readonly_storage:
2652             return 0
2653-        return fileutil.get_available_space(self.storedir, self.reserved_space)
2654+        return fileutil.get_available_space(self.sharedir, self.reserved_space)
2655 
2656     def allocated_size(self):
2657         space = 0
2658hunk ./src/allmydata/storage/server.py 346
2659         self.count("writev")
2660         si_s = si_b2a(storageindex)
2661         log.msg("storage: slot_writev %s" % si_s)
2662-       
2663+
2664         (write_enabler, renew_secret, cancel_secret) = secrets
2665         # shares exist if there is a file for them
2666         bucketdir = si_si2dir(self.sharedir, storageindex)
2667}
2668[das/__init__.py
2669wilcoxjg@gmail.com**20110810173849
2670 Ignore-this: bdb730cba1d53d8827ef5fef65958471
2671] addfile ./src/allmydata/storage/backends/das/__init__.py
2672[test_backends.py: cleaned whitespace and removed unused variables
2673wilcoxjg@gmail.com**20110810201041
2674 Ignore-this: d000d4a7d3a0793464306e9d09437be6
2675] {
2676hunk ./src/allmydata/test/test_backends.py 13
2677 from allmydata.storage.common import si_si2dir
2678 # The following share file content was generated with
2679 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
2680-# with share data == 'a'. The total size of this input
2681+# with share data == 'a'. The total size of this input
2682 # is 85 bytes.
2683 shareversionnumber = '\x00\x00\x00\x01'
2684 sharedatalength = '\x00\x00\x00\x01'
2685hunk ./src/allmydata/test/test_backends.py 29
2686     cancelsecret + expirationtime + nextlease
2687 share_data = containerdata + client_data
2688 testnodeid = 'testnodeidxxxxxxxxxx'
2689-expiration_policy = {'enabled' : False,
2690+expiration_policy = {'enabled' : False,
2691                      'mode' : 'age',
2692                      'override_lease_duration' : None,
2693                      'cutoff_date' : None,
2694hunk ./src/allmydata/test/test_backends.py 37
2695 
2696 
2697 class MockFileSystem(unittest.TestCase):
2698-    """ I simulate a filesystem that the code under test can use. I simulate
2699-    just the parts of the filesystem that the current implementation of DAS
2700+    """ I simulate a filesystem that the code under test can use. I simulate
2701+    just the parts of the filesystem that the current implementation of DAS
2702     backend needs. """
2703     def setUp(self):
2704         # Make patcher, patch, and make effects for fs using functions.
2705hunk ./src/allmydata/test/test_backends.py 43
2706         msg( "%s.setUp()" % (self,))
2707-        self.mockedfilepaths = {}
2708+        self.mockedfilepaths = {}
2709         #keys are pathnames, values are MockFilePath objects. This is necessary because
2710         #MockFilePath behavior sometimes depends on the filesystem. Where it does,
2711         #self.mockedfilepaths has the relevent info.
2712hunk ./src/allmydata/test/test_backends.py 56
2713         self.sharefinalname = self.sharedirfinalname.child('0')
2714 
2715         self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
2716-        FakePath = self.FilePathFake.__enter__()
2717+        self.FilePathFake.__enter__()
2718 
2719         self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
2720         FakeBCC = self.BCountingCrawler.__enter__()
2721hunk ./src/allmydata/test/test_backends.py 89
2722 
2723     def tearDown(self):
2724         msg( "%s.tearDown()" % (self,))
2725-        FakePath = self.FilePathFake.__exit__()       
2726+        self.FilePathFake.__exit__()
2727         self.mockedfilepaths = {}
2728 
2729 
2730hunk ./src/allmydata/test/test_backends.py 116
2731         self.mockedfilepaths[self.path].fileobject = self.fileobject
2732         self.mockedfilepaths[self.path].existance = self.existance
2733         self.setparents()
2734-       
2735+
2736     def create(self):
2737         # This method chokes if there's a pre-existing file!
2738         if self.mockedfilepaths[self.path].fileobject:
2739hunk ./src/allmydata/test/test_backends.py 122
2740             raise OSError
2741         else:
2742-            self.fileobject = MockFileObject(contentstring)
2743             self.existance = True
2744             self.mockedfilepaths[self.path].fileobject = self.fileobject
2745             self.mockedfilepaths[self.path].existance = self.existance
2746hunk ./src/allmydata/test/test_backends.py 125
2747-            self.setparents()       
2748+            self.setparents()
2749 
2750     def open(self, mode='r'):
2751         # XXX Makes no use of mode.
2752hunk ./src/allmydata/test/test_backends.py 151
2753         childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
2754         childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
2755         self.spawn = frozenset(childrenfromffs)
2756-        return self.spawn 
2757+        return self.spawn
2758 
2759     def parent(self):
2760         if self.mockedfilepaths.has_key(self.antecedent):
2761hunk ./src/allmydata/test/test_backends.py 163
2762     def parents(self):
2763         antecedents = []
2764         def f(fps, antecedents):
2765-            newfps = os.path.split(fps)[0]
2766+            newfps = os.path.split(fps)[0]
2767             if newfps:
2768                 antecedents.append(newfps)
2769                 f(newfps, antecedents)
2770hunk ./src/allmydata/test/test_backends.py 256
2771     @mock.patch('os.listdir')
2772     @mock.patch('os.path.isdir')
2773     def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
2774-        """ Write a new share. """
2775+        """ Write a new share. This tests that StorageServer's remote_allocate_buckets generates the correct return types when given test-vector arguments.  that bs is of the correct type is verified by bs[0] exercising remote_write without error. """
2776 
2777         alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2778         bs[0].remote_write(0, 'a')
2779hunk ./src/allmydata/test/test_backends.py 275
2780 
2781 
2782 class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
2783-    """ This tests both the StorageServer and the DAS backend together. """   
2784+    """ This tests both the StorageServer and the DAS backend together. """
2785     def setUp(self):
2786         MockFileSystem.setUp(self)
2787         try:
2788hunk ./src/allmydata/test/test_backends.py 292
2789     @mock.patch('allmydata.util.fileutil.get_available_space')
2790     def test_out_of_space(self, mockget_available_space, mocktime):
2791         mocktime.return_value = 0
2792-       
2793+
2794         def call_get_available_space(dir, reserve):
2795             return 0
2796 
2797hunk ./src/allmydata/test/test_backends.py 310
2798         mocktime.return_value = 0
2799         # Inspect incoming and fail unless it's empty.
2800         incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
2801-       
2802+
2803         self.failUnlessReallyEqual(incomingset, frozenset())
2804hunk ./src/allmydata/test/test_backends.py 312
2805-       
2806+
2807         # Populate incoming with the sharenum: 0.
2808         alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
2809 
2810hunk ./src/allmydata/test/test_backends.py 329
2811         # has been called.
2812         self.failIf(bsa)
2813 
2814-        # Test allocated size.
2815+        # Test allocated size.
2816         spaceint = self.ss.allocated_size()
2817         self.failUnlessReallyEqual(spaceint, 1)
2818 
2819hunk ./src/allmydata/test/test_backends.py 335
2820         # Write 'a' to shnum 0. Only tested together with close and read.
2821         bs[0].remote_write(0, 'a')
2822-       
2823+
2824         # Preclose: Inspect final, failUnless nothing there.
2825         self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
2826         bs[0].remote_close()
2827hunk ./src/allmydata/test/test_backends.py 349
2828         # Exercise the case that the share we're asking to allocate is
2829         # already (completely) uploaded.
2830         self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2831-       
2832+
2833 
2834     def test_read_old_share(self):
2835         """ This tests whether the code correctly finds and reads
2836hunk ./src/allmydata/test/test_backends.py 360
2837         StorageServer object. """
2838         # Contruct a file with the appropriate contents in the mockfilesystem.
2839         datalen = len(share_data)
2840-        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2841+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2842         finalhome.setContent(share_data)
2843 
2844         # Now begin the test.
2845}
2846[test_backends.py, backends/das -> backends/disk: renaming backend das to disk
2847wilcoxjg@gmail.com**20110829184834
2848 Ignore-this: c65f84cceb14e6001c6f6b1ddc9b508d
2849] {
2850move ./src/allmydata/storage/backends/das ./src/allmydata/storage/backends/disk
2851hunk ./src/allmydata/storage/backends/disk/core.py 22
2852 from allmydata.storage.immutable import BucketWriter, BucketReader
2853 from allmydata.storage.crawler import BucketCountingCrawler
2854 from allmydata.util.hashutil import constant_time_compare
2855-from allmydata.storage.backends.das.expirer import LeaseCheckingCrawler
2856+from allmydata.storage.backends.disk.expirer import LeaseCheckingCrawler
2857 _pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
2858 
2859 # storage/
2860hunk ./src/allmydata/storage/backends/disk/core.py 37
2861 # $SHARENUM matches this regex:
2862 NUM_RE=re.compile("^[0-9]+$")
2863 
2864-class DASCore(Backend):
2865+class DiskCore(Backend):
2866     implements(IStorageBackend)
2867     def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
2868         Backend.__init__(self)
2869hunk ./src/allmydata/test/test_backends.py 8
2870 import mock
2871 # This is the code that we're going to be testing.
2872 from allmydata.storage.server import StorageServer
2873-from allmydata.storage.backends.das.core import DASCore
2874+from allmydata.storage.backends.disk.core import DiskCore
2875 from allmydata.storage.backends.null.core import NullCore
2876 from allmydata.storage.common import si_si2dir
2877 # The following share file content was generated with
2878hunk ./src/allmydata/test/test_backends.py 38
2879 
2880 class MockFileSystem(unittest.TestCase):
2881     """ I simulate a filesystem that the code under test can use. I simulate
2882-    just the parts of the filesystem that the current implementation of DAS
2883+    just the parts of the filesystem that the current implementation of Disk
2884     backend needs. """
2885     def setUp(self):
2886         # Make patcher, patch, and make effects for fs using functions.
2887hunk ./src/allmydata/test/test_backends.py 55
2888         self.shareincomingname = self.sharedirincomingname.child('0')
2889         self.sharefinalname = self.sharedirfinalname.child('0')
2890 
2891-        self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
2892+        self.FilePathFake = mock.patch('allmydata.storage.backends.disk.core.FilePath', new = MockFilePath )
2893         self.FilePathFake.__enter__()
2894 
2895hunk ./src/allmydata/test/test_backends.py 58
2896-        self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
2897+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.disk.core.BucketCountingCrawler')
2898         FakeBCC = self.BCountingCrawler.__enter__()
2899         FakeBCC.side_effect = self.call_FakeBCC
2900 
2901hunk ./src/allmydata/test/test_backends.py 62
2902-        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.das.core.LeaseCheckingCrawler')
2903+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.disk.core.LeaseCheckingCrawler')
2904         FakeLCC = self.LeaseCheckingCrawler.__enter__()
2905         FakeLCC.side_effect = self.call_FakeLCC
2906 
2907hunk ./src/allmydata/test/test_backends.py 70
2908         GetSpace = self.get_available_space.__enter__()
2909         GetSpace.side_effect = self.call_get_available_space
2910 
2911-        self.statforsize = mock.patch('allmydata.storage.backends.das.core.filepath.stat')
2912+        self.statforsize = mock.patch('allmydata.storage.backends.disk.core.filepath.stat')
2913         getsize = self.statforsize.__enter__()
2914         getsize.side_effect = self.call_statforsize
2915 
2916hunk ./src/allmydata/test/test_backends.py 271
2917         """ This tests whether a server instance can be constructed with a
2918         filesystem backend. To pass the test, it mustn't use the filesystem
2919         outside of its configured storedir. """
2920-        StorageServer(testnodeid, backend=DASCore(self.storedir, expiration_policy))
2921+        StorageServer(testnodeid, backend=DiskCore(self.storedir, expiration_policy))
2922 
2923 
2924 class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
2925hunk ./src/allmydata/test/test_backends.py 275
2926-    """ This tests both the StorageServer and the DAS backend together. """
2927+    """ This tests both the StorageServer and the Disk backend together. """
2928     def setUp(self):
2929         MockFileSystem.setUp(self)
2930         try:
2931hunk ./src/allmydata/test/test_backends.py 279
2932-            self.backend = DASCore(self.storedir, expiration_policy)
2933+            self.backend = DiskCore(self.storedir, expiration_policy)
2934             self.ss = StorageServer(testnodeid, self.backend)
2935 
2936hunk ./src/allmydata/test/test_backends.py 282
2937-            self.backendwithreserve = DASCore(self.storedir, expiration_policy, reserved_space = 1)
2938+            self.backendwithreserve = DiskCore(self.storedir, expiration_policy, reserved_space = 1)
2939             self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
2940         except:
2941             MockFileSystem.tearDown(self)
2942}
2943
2944Context:
2945
2946[test_mutable.Update: only upload the files needed for each test. refs #1500
2947Brian Warner <warner@lothar.com>**20110829072717
2948 Ignore-this: 4d2ab4c7523af9054af7ecca9c3d9dc7
2949 
2950 This first step shaves 15% off the runtime: from 139s to 119s on my laptop.
2951 It also fixes a couple of places where a Deferred was being dropped, which
2952 would cause two tests to run in parallel and also confuse error reporting.
2953]
2954[Let Uploader retain History instead of passing it into upload(). Fixes #1079.
2955Brian Warner <warner@lothar.com>**20110829063246
2956 Ignore-this: 3902c58ec12bd4b2d876806248e19f17
2957 
2958 This consistently records all immutable uploads in the Recent Uploads And
2959 Downloads page, regardless of code path. Previously, certain webapi upload
2960 operations (like PUT /uri/$DIRCAP/newchildname) failed to pass the History
2961 object and were left out.
2962]
2963[Fix mutable publish/retrieve timing status displays. Fixes #1505.
2964Brian Warner <warner@lothar.com>**20110828232221
2965 Ignore-this: 4080ce065cf481b2180fd711c9772dd6
2966 
2967 publish:
2968 * encrypt and encode times are cumulative, not just current-segment
2969 
2970 retrieve:
2971 * same for decrypt and decode times
2972 * update "current status" to include segment number
2973 * set status to Finished/Failed when download is complete
2974 * set progress to 1.0 when complete
2975 
2976 More improvements to consider:
2977 * progress is currently 0% or 100%: should calculate how many segments are
2978   involved (remembering retrieve can be less than the whole file) and set it
2979   to a fraction
2980 * "fetch" time is fuzzy: what we want is to know how much of the delay is not
2981   our own fault, but since we do decode/decrypt work while waiting for more
2982   shares, it's not straightforward
2983]
2984[Teach 'tahoe debug catalog-shares about MDMF. Closes #1507.
2985Brian Warner <warner@lothar.com>**20110828080931
2986 Ignore-this: 56ef2951db1a648353d7daac6a04c7d1
2987]
2988[debug.py: remove some dead comments
2989Brian Warner <warner@lothar.com>**20110828074556
2990 Ignore-this: 40e74040dd4d14fd2f4e4baaae506b31
2991]
2992[hush pyflakes
2993Brian Warner <warner@lothar.com>**20110828074254
2994 Ignore-this: bef9d537a969fa82fe4decc4ba2acb09
2995]
2996[MutableFileNode.set_downloader_hints: never depend upon order of dict.values()
2997Brian Warner <warner@lothar.com>**20110828074103
2998 Ignore-this: caaf1aa518dbdde4d797b7f335230faa
2999 
3000 The old code was calculating the "extension parameters" (a list) from the
3001 downloader hints (a dictionary) with hints.values(), which is not stable, and
3002 would result in corrupted filecaps (with the 'k' and 'segsize' hints
3003 occasionally swapped). The new code always uses [k,segsize].
3004]
3005[layout.py: fix MDMF share layout documentation
3006Brian Warner <warner@lothar.com>**20110828073921
3007 Ignore-this: 3f13366fed75b5e31b51ae895450a225
3008]
3009[teach 'tahoe debug dump-share' about MDMF and offsets. refs #1507
3010Brian Warner <warner@lothar.com>**20110828073834
3011 Ignore-this: 3a9d2ef9c47a72bf1506ba41199a1dea
3012]
3013[test_mutable.Version.test_debug: use splitlines() to fix buildslaves
3014Brian Warner <warner@lothar.com>**20110828064728
3015 Ignore-this: c7f6245426fc80b9d1ae901d5218246a
3016 
3017 Any slave running in a directory with spaces in the name was miscounting
3018 shares, causing the test to fail.
3019]
3020[test_mutable.Version: exercise 'tahoe debug find-shares' on MDMF. refs #1507
3021Brian Warner <warner@lothar.com>**20110828005542
3022 Ignore-this: cb20bea1c28bfa50a72317d70e109672
3023 
3024 Also changes NoNetworkGrid to put shares in storage/shares/ .
3025]
3026[test_mutable.py: oops, missed a .todo
3027Brian Warner <warner@lothar.com>**20110828002118
3028 Ignore-this: fda09ae86481352b7a627c278d2a3940
3029]
3030[test_mutable: merge davidsarah's patch with my Version refactorings
3031warner@lothar.com**20110827235707
3032 Ignore-this: b5aaf481c90d99e33827273b5d118fd0
3033]
3034[Make the immutable/read-only constraint checking for MDMF URIs identical to that for SSK URIs. refs #393
3035david-sarah@jacaranda.org**20110823012720
3036 Ignore-this: e1f59d7ff2007c81dbef2aeb14abd721
3037]
3038[Additional tests for MDMF URIs and for zero-length files. refs #393
3039david-sarah@jacaranda.org**20110823011532
3040 Ignore-this: a7cc0c09d1d2d72413f9cd227c47a9d5
3041]
3042[Additional tests for zero-length partial reads and updates to mutable versions. refs #393
3043david-sarah@jacaranda.org**20110822014111
3044 Ignore-this: 5fc6f4d06e11910124e4a277ec8a43ea
3045]
3046[test_mutable.Version: factor out some expensive uploads, save 25% runtime
3047Brian Warner <warner@lothar.com>**20110827232737
3048 Ignore-this: ea37383eb85ea0894b254fe4dfb45544
3049]
3050[SDMF: update filenode with correct k/N after Retrieve. Fixes #1510.
3051Brian Warner <warner@lothar.com>**20110827225031
3052 Ignore-this: b50ae6e1045818c400079f118b4ef48
3053 
3054 Without this, we get a regression when modifying a mutable file that was
3055 created with more shares (larger N) than our current tahoe.cfg . The
3056 modification attempt creates new versions of the (0,1,..,newN-1) shares, but
3057 leaves the old versions of the (newN,..,oldN-1) shares alone (and throws a
3058 assertion error in SDMFSlotWriteProxy.finish_publishing in the process).
3059 
3060 The mixed versions that result (some shares with e.g. N=10, some with N=20,
3061 such that both versions are recoverable) cause problems for the Publish code,
3062 even before MDMF landed. Might be related to refs #1390 and refs #1042.
3063]
3064[layout.py: annotate assertion to figure out 'tahoe backup' failure
3065Brian Warner <warner@lothar.com>**20110827195253
3066 Ignore-this: 9b92b954e3ed0d0f80154fff1ff674e5
3067]
3068[Add 'tahoe debug dump-cap' support for MDMF, DIR2-CHK, DIR2-MDMF. refs #1507.
3069Brian Warner <warner@lothar.com>**20110827195048
3070 Ignore-this: 61c6af5e33fc88e0251e697a50addb2c
3071 
3072 This also adds tests for all those cases, and fixes an omission in uri.py
3073 that broke parsing of DIR2-MDMF-Verifier and DIR2-CHK-Verifier.
3074]
3075[MDMF: more writable/writeable consistentifications
3076warner@lothar.com**20110827190602
3077 Ignore-this: 22492a9e20c1819ddb12091062888b55
3078]
3079[MDMF: s/Writable/Writeable/g, for consistency with existing SDMF code
3080warner@lothar.com**20110827183357
3081 Ignore-this: 9dd312acedbdb2fc2f7bef0d0fb17c0b
3082]
3083[setup.cfg: remove no-longer-supported test_mac_diskimage alias. refs #1479
3084david-sarah@jacaranda.org**20110826230345
3085 Ignore-this: 40e908b8937322a290fb8012bfcad02a
3086]
3087[test_mutable.Update: increase timeout from 120s to 400s, slaves are failing
3088Brian Warner <warner@lothar.com>**20110825230140
3089 Ignore-this: 101b1924a30cdbda9b2e419e95ca15ec
3090]
3091[tests: fix check_memory test
3092zooko@zooko.com**20110825201116
3093 Ignore-this: 4d66299fa8cb61d2ca04b3f45344d835
3094 fixes #1503
3095]
3096[TAG allmydata-tahoe-1.9.0a1
3097warner@lothar.com**20110825161122
3098 Ignore-this: 3cbf49f00dbda58189f893c427f65605
3099]
3100Patch bundle hash:
3101e4d96daddf4df85d6d078a902322e65b845ea192