Ticket #1465: addresseszookocomment03_whitespace_pyflakes_20110810.darcs.patch

File addresseszookocomment03_whitespace_pyflakes_20110810.darcs.patch, 151.7 KB (added by Zancas, at 2011-08-10T20:19:29Z)

ugghh... more cleaning to do

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
46New patches:
47
48[storage: add tests of the new feature of having the storage backend in a separate object from the server
49wilcoxjg@gmail.com**20110809193910
50 Ignore-this: 72b64dab1a9ce668607a4ece4429e29a
51] {
52addfile ./src/allmydata/test/test_backends.py
53hunk ./src/allmydata/test/test_backends.py 1
54+import os, stat
55+from twisted.trial import unittest
56+from allmydata.util.log import msg
57+from allmydata.test.common_util import ReallyEqualMixin
58+import mock
59+# This is the code that we're going to be testing.
60+from allmydata.storage.server import StorageServer
61+from allmydata.storage.backends.das.core import DASCore
62+from allmydata.storage.backends.null.core import NullCore
63+from allmydata.storage.common import si_si2dir
64+# The following share file content was generated with
65+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
66+# with share data == 'a'. The total size of this input
67+# is 85 bytes.
68+shareversionnumber = '\x00\x00\x00\x01'
69+sharedatalength = '\x00\x00\x00\x01'
70+numberofleases = '\x00\x00\x00\x01'
71+shareinputdata = 'a'
72+ownernumber = '\x00\x00\x00\x00'
73+renewsecret  = 'x'*32
74+cancelsecret = 'y'*32
75+expirationtime = '\x00(\xde\x80'
76+nextlease = ''
77+containerdata = shareversionnumber + sharedatalength + numberofleases
78+client_data = shareinputdata + ownernumber + renewsecret + \
79+    cancelsecret + expirationtime + nextlease
80+share_data = containerdata + client_data
81+testnodeid = 'testnodeidxxxxxxxxxx'
82+expiration_policy = {'enabled' : False,
83+                     'mode' : 'age',
84+                     'override_lease_duration' : None,
85+                     'cutoff_date' : None,
86+                     'sharetypes' : None}
87+
88+
89+class MockFileSystem(unittest.TestCase):
90+    """ I simulate a filesystem that the code under test can use. I simulate
91+    just the parts of the filesystem that the current implementation of DAS
92+    backend needs. """
93+    def setUp(self):
94+        # Make patcher, patch, and make effects for fs using functions.
95+        msg( "%s.setUp()" % (self,))
96+        self.mockedfilepaths = {}
97+        #keys are pathnames, values are MockFilePath objects. This is necessary because
98+        #MockFilePath behavior sometimes depends on the filesystem. Where it does,
99+        #self.mockedfilepaths has the relevent info.
100+        self.storedir = MockFilePath('teststoredir', self.mockedfilepaths)
101+        self.basedir = self.storedir.child('shares')
102+        self.baseincdir = self.basedir.child('incoming')
103+        self.sharedirfinalname = self.basedir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
104+        self.sharedirincomingname = self.baseincdir.child('or').child('orsxg5dtorxxeylhmvpws3temv4a')
105+        self.shareincomingname = self.sharedirincomingname.child('0')
106+        self.sharefinalname = self.sharedirfinalname.child('0')
107+
108+        self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
109+        FakePath = self.FilePathFake.__enter__()
110+
111+        self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
112+        FakeBCC = self.BCountingCrawler.__enter__()
113+        FakeBCC.side_effect = self.call_FakeBCC
114+
115+        self.LeaseCheckingCrawler = mock.patch('allmydata.storage.backends.das.core.LeaseCheckingCrawler')
116+        FakeLCC = self.LeaseCheckingCrawler.__enter__()
117+        FakeLCC.side_effect = self.call_FakeLCC
118+
119+        self.get_available_space = mock.patch('allmydata.util.fileutil.get_available_space')
120+        GetSpace = self.get_available_space.__enter__()
121+        GetSpace.side_effect = self.call_get_available_space
122+
123+        self.statforsize = mock.patch('allmydata.storage.backends.das.core.filepath.stat')
124+        getsize = self.statforsize.__enter__()
125+        getsize.side_effect = self.call_statforsize
126+
127+    def call_FakeBCC(self, StateFile):
128+        return MockBCC()
129+
130+    def call_FakeLCC(self, StateFile, HistoryFile, ExpirationPolicy):
131+        return MockLCC()
132+
133+    def call_get_available_space(self, storedir, reservedspace):
134+        # The input vector has an input size of 85.
135+        return 85 - reservedspace
136+
137+    def call_statforsize(self, fakefpname):
138+        return self.mockedfilepaths[fakefpname].fileobject.size()
139+
140+    def tearDown(self):
141+        msg( "%s.tearDown()" % (self,))
142+        FakePath = self.FilePathFake.__exit__()       
143+        self.mockedfilepaths = {}
144+
145+
146+class MockFilePath:
147+    def __init__(self, pathstring, ffpathsenvironment, existance=False):
148+        #  I can't jsut make the values MockFileObjects because they may be directories.
149+        self.mockedfilepaths = ffpathsenvironment
150+        self.path = pathstring
151+        self.existance = existance
152+        if not self.mockedfilepaths.has_key(self.path):
153+            #  The first MockFilePath object is special
154+            self.mockedfilepaths[self.path] = self
155+            self.fileobject = None
156+        else:
157+            self.fileobject = self.mockedfilepaths[self.path].fileobject
158+        self.spawn = {}
159+        self.antecedent = os.path.dirname(self.path)
160+
161+    def setContent(self, contentstring):
162+        # This method rewrites the data in the file that corresponds to its path
163+        # name whether it preexisted or not.
164+        self.fileobject = MockFileObject(contentstring)
165+        self.existance = True
166+        self.mockedfilepaths[self.path].fileobject = self.fileobject
167+        self.mockedfilepaths[self.path].existance = self.existance
168+        self.setparents()
169+       
170+    def create(self):
171+        # This method chokes if there's a pre-existing file!
172+        if self.mockedfilepaths[self.path].fileobject:
173+            raise OSError
174+        else:
175+            self.fileobject = MockFileObject(contentstring)
176+            self.existance = True
177+            self.mockedfilepaths[self.path].fileobject = self.fileobject
178+            self.mockedfilepaths[self.path].existance = self.existance
179+            self.setparents()       
180+
181+    def open(self, mode='r'):
182+        # XXX Makes no use of mode.
183+        if not self.mockedfilepaths[self.path].fileobject:
184+            # If there's no fileobject there already then make one and put it there.
185+            self.fileobject = MockFileObject()
186+            self.existance = True
187+            self.mockedfilepaths[self.path].fileobject = self.fileobject
188+            self.mockedfilepaths[self.path].existance = self.existance
189+        else:
190+            # Otherwise get a ref to it.
191+            self.fileobject = self.mockedfilepaths[self.path].fileobject
192+            self.existance = self.mockedfilepaths[self.path].existance
193+        return self.fileobject.open(mode)
194+
195+    def child(self, childstring):
196+        arg2child = os.path.join(self.path, childstring)
197+        child = MockFilePath(arg2child, self.mockedfilepaths)
198+        return child
199+
200+    def children(self):
201+        childrenfromffs = [ffp for ffp in self.mockedfilepaths.values() if ffp.path.startswith(self.path)]
202+        childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
203+        childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
204+        self.spawn = frozenset(childrenfromffs)
205+        return self.spawn 
206+
207+    def parent(self):
208+        if self.mockedfilepaths.has_key(self.antecedent):
209+            parent = self.mockedfilepaths[self.antecedent]
210+        else:
211+            parent = MockFilePath(self.antecedent, self.mockedfilepaths)
212+        return parent
213+
214+    def parents(self):
215+        antecedents = []
216+        def f(fps, antecedents):
217+            newfps = os.path.split(fps)[0]
218+            if newfps:
219+                antecedents.append(newfps)
220+                f(newfps, antecedents)
221+        f(self.path, antecedents)
222+        return antecedents
223+
224+    def setparents(self):
225+        for fps in self.parents():
226+            if not self.mockedfilepaths.has_key(fps):
227+                self.mockedfilepaths[fps] = MockFilePath(fps, self.mockedfilepaths, exists=True)
228+
229+    def basename(self):
230+        return os.path.split(self.path)[1]
231+
232+    def moveTo(self, newffp):
233+        #  XXX Makes no distinction between file and directory arguments, this is deviation from filepath.moveTo
234+        if self.mockedfilepaths[newffp.path].exists():
235+            raise OSError
236+        else:
237+            self.mockedfilepaths[newffp.path] = self
238+            self.path = newffp.path
239+
240+    def getsize(self):
241+        return self.fileobject.getsize()
242+
243+    def exists(self):
244+        return self.existance
245+
246+    def isdir(self):
247+        return True
248+
249+    def makedirs(self):
250+        # XXX These methods assume that fp_<FOO> functions in fileutil will be tested elsewhere!
251+        pass
252+
253+    def remove(self):
254+        pass
255+
256+
257+class MockFileObject:
258+    def __init__(self, contentstring=''):
259+        self.buffer = contentstring
260+        self.pos = 0
261+    def open(self, mode='r'):
262+        return self
263+    def write(self, instring):
264+        begin = self.pos
265+        padlen = begin - len(self.buffer)
266+        if padlen > 0:
267+            self.buffer += '\x00' * padlen
268+        end = self.pos + len(instring)
269+        self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
270+        self.pos = end
271+    def close(self):
272+        self.pos = 0
273+    def seek(self, pos):
274+        self.pos = pos
275+    def read(self, numberbytes):
276+        return self.buffer[self.pos:self.pos+numberbytes]
277+    def tell(self):
278+        return self.pos
279+    def size(self):
280+        # XXX This method A: Is not to be found in a real file B: Is part of a wild-mung-up of filepath.stat!
281+        # XXX Finally we shall hopefully use a getsize method soon, must consult first though.
282+        # Hmmm...  perhaps we need to sometimes stat the address when there's not a mockfileobject present?
283+        return {stat.ST_SIZE:len(self.buffer)}
284+    def getsize(self):
285+        return len(self.buffer)
286+
287+class MockBCC:
288+    def setServiceParent(self, Parent):
289+        pass
290+
291+
292+class MockLCC:
293+    def setServiceParent(self, Parent):
294+        pass
295+
296+
297+class TestServerWithNullBackend(unittest.TestCase, ReallyEqualMixin):
298+    """ NullBackend is just for testing and executable documentation, so
299+    this test is actually a test of StorageServer in which we're using
300+    NullBackend as helper code for the test, rather than a test of
301+    NullBackend. """
302+    def setUp(self):
303+        self.ss = StorageServer(testnodeid, backend=NullCore())
304+
305+    @mock.patch('os.mkdir')
306+    @mock.patch('__builtin__.open')
307+    @mock.patch('os.listdir')
308+    @mock.patch('os.path.isdir')
309+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
310+        """ Write a new share. """
311+
312+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
313+        bs[0].remote_write(0, 'a')
314+        self.failIf(mockisdir.called)
315+        self.failIf(mocklistdir.called)
316+        self.failIf(mockopen.called)
317+        self.failIf(mockmkdir.called)
318+
319+
320+class TestServerConstruction(MockFileSystem, ReallyEqualMixin):
321+    def test_create_server_fs_backend(self):
322+        """ This tests whether a server instance can be constructed with a
323+        filesystem backend. To pass the test, it mustn't use the filesystem
324+        outside of its configured storedir. """
325+        StorageServer(testnodeid, backend=DASCore(self.storedir, expiration_policy))
326+
327+
328+class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
329+    """ This tests both the StorageServer and the DAS backend together. """   
330+    def setUp(self):
331+        MockFileSystem.setUp(self)
332+        try:
333+            self.backend = DASCore(self.storedir, expiration_policy)
334+            self.ss = StorageServer(testnodeid, self.backend)
335+
336+            self.backendwithreserve = DASCore(self.storedir, expiration_policy, reserved_space = 1)
337+            self.sswithreserve = StorageServer(testnodeid, self.backendwithreserve)
338+        except:
339+            MockFileSystem.tearDown(self)
340+            raise
341+
342+    @mock.patch('time.time')
343+    @mock.patch('allmydata.util.fileutil.get_available_space')
344+    def test_out_of_space(self, mockget_available_space, mocktime):
345+        mocktime.return_value = 0
346+       
347+        def call_get_available_space(dir, reserve):
348+            return 0
349+
350+        mockget_available_space.side_effect = call_get_available_space
351+        alreadygotc, bsc = self.sswithreserve.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
352+        self.failUnlessReallyEqual(bsc, {})
353+
354+    @mock.patch('time.time')
355+    def test_write_and_read_share(self, mocktime):
356+        """
357+        Write a new share, read it, and test the server's (and FS backend's)
358+        handling of simultaneous and successive attempts to write the same
359+        share.
360+        """
361+        mocktime.return_value = 0
362+        # Inspect incoming and fail unless it's empty.
363+        incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
364+       
365+        self.failUnlessReallyEqual(incomingset, frozenset())
366+       
367+        # Populate incoming with the sharenum: 0.
368+        alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
369+
370+        # This is a transparent-box test: Inspect incoming and fail unless the sharenum: 0 is listed there.
371+        self.failUnlessReallyEqual(self.ss.backend.get_incoming_shnums('teststorage_index'), frozenset((0,)))
372+
373+
374+
375+        # Attempt to create a second share writer with the same sharenum.
376+        alreadygota, bsa = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
377+
378+        # Show that no sharewriter results from a remote_allocate_buckets
379+        # with the same si and sharenum, until BucketWriter.remote_close()
380+        # has been called.
381+        self.failIf(bsa)
382+
383+        # Test allocated size.
384+        spaceint = self.ss.allocated_size()
385+        self.failUnlessReallyEqual(spaceint, 1)
386+
387+        # Write 'a' to shnum 0. Only tested together with close and read.
388+        bs[0].remote_write(0, 'a')
389+       
390+        # Preclose: Inspect final, failUnless nothing there.
391+        self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
392+        bs[0].remote_close()
393+
394+        # Postclose: (Omnibus) failUnless written data is in final.
395+        sharesinfinal = list(self.backend.get_shares('teststorage_index'))
396+        self.failUnlessReallyEqual(len(sharesinfinal), 1)
397+        contents = sharesinfinal[0].read_share_data(0, 73)
398+        self.failUnlessReallyEqual(contents, client_data)
399+
400+        # Exercise the case that the share we're asking to allocate is
401+        # already (completely) uploaded.
402+        self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
403+       
404+
405+    def test_read_old_share(self):
406+        """ This tests whether the code correctly finds and reads
407+        shares written out by old (Tahoe-LAFS <= v1.8.2)
408+        servers. There is a similar test in test_download, but that one
409+        is from the perspective of the client and exercises a deeper
410+        stack of code. This one is for exercising just the
411+        StorageServer object. """
412+        # Contruct a file with the appropriate contents in the mockfilesystem.
413+        datalen = len(share_data)
414+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
415+        finalhome.setContent(share_data)
416+
417+        # Now begin the test.
418+        bs = self.ss.remote_get_buckets('teststorage_index')
419+
420+        self.failUnlessEqual(len(bs), 1)
421+        b = bs['0']
422+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
423+        self.failUnlessReallyEqual(b.remote_read(0, datalen), client_data)
424+        # If you try to read past the end you get the as much data as is there.
425+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), client_data)
426+        # If you start reading past the end of the file you get the empty string.
427+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
428}
429[Added directories and new modules for the null backend
430wilcoxjg@gmail.com**20110809200929
431 Ignore-this: f5dfa418afced5141eb9247a9908109e
432] {
433hunk ./src/allmydata/interfaces.py 270
434         store that on disk.
435         """
436 
437+class IStorageBackend(Interface):
438+    """
439+    Objects of this kind live on the server side and are used by the
440+    storage server object.
441+    """
442+    def get_available_space(self, reserved_space):
443+        """ Returns available space for share storage in bytes, or
444+        None if this information is not available or if the available
445+        space is unlimited.
446+
447+        If the backend is configured for read-only mode then this will
448+        return 0.
449+
450+        reserved_space is how many bytes to subtract from the answer, so
451+        you can pass how many bytes you would like to leave unused on this
452+        filesystem as reserved_space. """
453+
454+    def get_bucket_shares(self):
455+        """XXX"""
456+
457+    def get_share(self):
458+        """XXX"""
459+
460+    def make_bucket_writer(self):
461+        """XXX"""
462+
463+class IStorageBackendShare(Interface):
464+    """
465+    This object contains as much as all of the share data.  It is intended
466+    for lazy evaluation such that in many use cases substantially less than
467+    all of the share data will be accessed.
468+    """
469+    def is_complete(self):
470+        """
471+        Returns the share state, or None if the share does not exist.
472+        """
473+
474 class IStorageBucketWriter(Interface):
475     """
476     Objects of this kind live on the client side.
477adddir ./src/allmydata/storage/backends
478addfile ./src/allmydata/storage/backends/base.py
479hunk ./src/allmydata/storage/backends/base.py 1
480+from twisted.application import service
481+
482+class Backend(service.MultiService):
483+    def __init__(self):
484+        service.MultiService.__init__(self)
485adddir ./src/allmydata/storage/backends/null
486addfile ./src/allmydata/storage/backends/null/core.py
487hunk ./src/allmydata/storage/backends/null/core.py 1
488+from allmydata.storage.backends.base import Backend
489+from allmydata.storage.immutable import BucketWriter, BucketReader
490+
491+class NullCore(Backend):
492+    def __init__(self):
493+        Backend.__init__(self)
494+
495+    def get_available_space(self):
496+        return None
497+
498+    def get_shares(self, storage_index):
499+        return set()
500+
501+    def get_share(self, storage_index, sharenum):
502+        return None
503+
504+    def make_bucket_writer(self, storageindex, shnum, max_space_per_bucket, lease_info, canary):
505+        immutableshare = ImmutableShare()
506+        return BucketWriter(self.ss, immutableshare, max_space_per_bucket, lease_info, canary)
507+
508+    def set_storage_server(self, ss):
509+        self.ss = ss
510+
511+    def get_incoming_shnums(self, storageindex):
512+        return frozenset()
513+
514+class ImmutableShare:
515+    sharetype = "immutable"
516+
517+    def __init__(self):
518+        """ If max_size is not None then I won't allow more than
519+        max_size to be written to me. If create=True then max_size
520+        must not be None. """
521+        pass
522+
523+    def get_shnum(self):
524+        return self.shnum
525+
526+    def unlink(self):
527+        os.unlink(self.fname)
528+
529+    def read_share_data(self, offset, length):
530+        precondition(offset >= 0)
531+        # Reads beyond the end of the data are truncated. Reads that start
532+        # beyond the end of the data return an empty string.
533+        seekpos = self._data_offset+offset
534+        fsize = os.path.getsize(self.fname)
535+        actuallength = max(0, min(length, fsize-seekpos))
536+        if actuallength == 0:
537+            return ""
538+        f = open(self.fname, 'rb')
539+        f.seek(seekpos)
540+        return f.read(actuallength)
541+
542+    def write_share_data(self, offset, data):
543+        pass
544+
545+    def _write_lease_record(self, f, lease_number, lease_info):
546+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
547+        f.seek(offset)
548+        assert f.tell() == offset
549+        f.write(lease_info.to_immutable_data())
550+
551+    def _read_num_leases(self, f):
552+        f.seek(0x08)
553+        (num_leases,) = struct.unpack(">L", f.read(4))
554+        return num_leases
555+
556+    def _write_num_leases(self, f, num_leases):
557+        f.seek(0x08)
558+        f.write(struct.pack(">L", num_leases))
559+
560+    def _truncate_leases(self, f, num_leases):
561+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
562+
563+    def get_leases(self):
564+        """Yields a LeaseInfo instance for all leases."""
565+        f = open(self.fname, 'rb')
566+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
567+        f.seek(self._lease_offset)
568+        for i in range(num_leases):
569+            data = f.read(self.LEASE_SIZE)
570+            if data:
571+                yield LeaseInfo().from_immutable_data(data)
572+
573+    def add_lease(self, lease):
574+        pass
575+
576+    def renew_lease(self, renew_secret, new_expire_time):
577+        for i,lease in enumerate(self.get_leases()):
578+            if constant_time_compare(lease.renew_secret, renew_secret):
579+                # yup. See if we need to update the owner time.
580+                if new_expire_time > lease.expiration_time:
581+                    # yes
582+                    lease.expiration_time = new_expire_time
583+                    f = open(self.fname, 'rb+')
584+                    self._write_lease_record(f, i, lease)
585+                    f.close()
586+                return
587+        raise IndexError("unable to renew non-existent lease")
588+
589+    def add_or_renew_lease(self, lease_info):
590+        try:
591+            self.renew_lease(lease_info.renew_secret,
592+                             lease_info.expiration_time)
593+        except IndexError:
594+            self.add_lease(lease_info)
595+
596+
597+    def cancel_lease(self, cancel_secret):
598+        """Remove a lease with the given cancel_secret. If the last lease is
599+        cancelled, the file will be removed. Return the number of bytes that
600+        were freed (by truncating the list of leases, and possibly by
601+        deleting the file. Raise IndexError if there was no lease with the
602+        given cancel_secret.
603+        """
604+
605+        leases = list(self.get_leases())
606+        num_leases_removed = 0
607+        for i,lease in enumerate(leases):
608+            if constant_time_compare(lease.cancel_secret, cancel_secret):
609+                leases[i] = None
610+                num_leases_removed += 1
611+        if not num_leases_removed:
612+            raise IndexError("unable to find matching lease to cancel")
613+        if num_leases_removed:
614+            # pack and write out the remaining leases. We write these out in
615+            # the same order as they were added, so that if we crash while
616+            # doing this, we won't lose any non-cancelled leases.
617+            leases = [l for l in leases if l] # remove the cancelled leases
618+            f = open(self.fname, 'rb+')
619+            for i,lease in enumerate(leases):
620+                self._write_lease_record(f, i, lease)
621+            self._write_num_leases(f, len(leases))
622+            self._truncate_leases(f, len(leases))
623+            f.close()
624+        space_freed = self.LEASE_SIZE * num_leases_removed
625+        if not len(leases):
626+            space_freed += os.stat(self.fname)[stat.ST_SIZE]
627+            self.unlink()
628+        return space_freed
629}
630[changes to null/core.py and storage/common.py necessary for test with null backend to pass
631wilcoxjg@gmail.com**20110809201249
632 Ignore-this: 9ddcd79f9962550ed20518ae85b6b6b2
633] {
634hunk ./src/allmydata/storage/backends/null/core.py 3
635 from allmydata.storage.backends.base import Backend
636 from allmydata.storage.immutable import BucketWriter, BucketReader
637+from zope.interface import implements
638 
639 class NullCore(Backend):
640hunk ./src/allmydata/storage/backends/null/core.py 6
641+    implements(IStorageBackend)
642     def __init__(self):
643         Backend.__init__(self)
644 
645hunk ./src/allmydata/storage/backends/null/core.py 30
646         return frozenset()
647 
648 class ImmutableShare:
649+    implements(IStorageBackendShare)
650     sharetype = "immutable"
651 
652     def __init__(self):
653hunk ./src/allmydata/storage/common.py 19
654 def si_a2b(ascii_storageindex):
655     return base32.a2b(ascii_storageindex)
656 
657-def storage_index_to_dir(storageindex):
658+def si_si2dir(startfp, storageindex):
659     sia = si_b2a(storageindex)
660hunk ./src/allmydata/storage/common.py 21
661-    return os.path.join(sia[:2], sia)
662+    newfp = startfp.child(sia[:2])
663+    return newfp.child(sia)
664}
665[change storage/server.py to new "backend pluggable" version
666wilcoxjg@gmail.com**20110809201647
667 Ignore-this: 1b0c5f9e831641287992bf45af55246e
668] {
669hunk ./src/allmydata/storage/server.py 1
670-import os, re, weakref, struct, time
671+import os, weakref, struct, time
672 
673 from foolscap.api import Referenceable
674 from twisted.application import service
675hunk ./src/allmydata/storage/server.py 11
676 from allmydata.util import fileutil, idlib, log, time_format
677 import allmydata # for __full_version__
678 
679-from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
680-_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
681+from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
682+_pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
683 from allmydata.storage.lease import LeaseInfo
684 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
685      create_mutable_sharefile
686hunk ./src/allmydata/storage/server.py 16
687-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
688-from allmydata.storage.crawler import BucketCountingCrawler
689-from allmydata.storage.expirer import LeaseCheckingCrawler
690-
691-# storage/
692-# storage/shares/incoming
693-#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
694-#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
695-# storage/shares/$START/$STORAGEINDEX
696-# storage/shares/$START/$STORAGEINDEX/$SHARENUM
697-
698-# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
699-# base-32 chars).
700-
701-# $SHARENUM matches this regex:
702-NUM_RE=re.compile("^[0-9]+$")
703-
704-
705 
706 class StorageServer(service.MultiService, Referenceable):
707     implements(RIStorageServer, IStatsProducer)
708hunk ./src/allmydata/storage/server.py 20
709     name = 'storage'
710-    LeaseCheckerClass = LeaseCheckingCrawler
711 
712hunk ./src/allmydata/storage/server.py 21
713-    def __init__(self, storedir, nodeid, reserved_space=0,
714-                 discard_storage=False, readonly_storage=False,
715-                 stats_provider=None,
716-                 expiration_enabled=False,
717-                 expiration_mode="age",
718-                 expiration_override_lease_duration=None,
719-                 expiration_cutoff_date=None,
720-                 expiration_sharetypes=("mutable", "immutable")):
721+    def __init__(self, nodeid, backend, reserved_space=0,
722+                 readonly_storage=False,
723+                 stats_provider=None ):
724         service.MultiService.__init__(self)
725         assert isinstance(nodeid, str)
726         assert len(nodeid) == 20
727hunk ./src/allmydata/storage/server.py 28
728         self.my_nodeid = nodeid
729-        self.storedir = storedir
730-        sharedir = os.path.join(storedir, "shares")
731-        fileutil.make_dirs(sharedir)
732-        self.sharedir = sharedir
733-        # we don't actually create the corruption-advisory dir until necessary
734-        self.corruption_advisory_dir = os.path.join(storedir,
735-                                                    "corruption-advisories")
736-        self.reserved_space = int(reserved_space)
737-        self.no_storage = discard_storage
738-        self.readonly_storage = readonly_storage
739         self.stats_provider = stats_provider
740         if self.stats_provider:
741             self.stats_provider.register_producer(self)
742hunk ./src/allmydata/storage/server.py 31
743-        self.incomingdir = os.path.join(sharedir, 'incoming')
744-        self._clean_incomplete()
745-        fileutil.make_dirs(self.incomingdir)
746         self._active_writers = weakref.WeakKeyDictionary()
747hunk ./src/allmydata/storage/server.py 32
748+        self.backend = backend
749+        self.backend.setServiceParent(self)
750+        self.backend.set_storage_server(self)
751         log.msg("StorageServer created", facility="tahoe.storage")
752 
753hunk ./src/allmydata/storage/server.py 37
754-        if reserved_space:
755-            if self.get_available_space() is None:
756-                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",
757-                        umin="0wZ27w", level=log.UNUSUAL)
758-
759         self.latencies = {"allocate": [], # immutable
760                           "write": [],
761                           "close": [],
762hunk ./src/allmydata/storage/server.py 48
763                           "renew": [],
764                           "cancel": [],
765                           }
766-        self.add_bucket_counter()
767-
768-        statefile = os.path.join(self.storedir, "lease_checker.state")
769-        historyfile = os.path.join(self.storedir, "lease_checker.history")
770-        klass = self.LeaseCheckerClass
771-        self.lease_checker = klass(self, statefile, historyfile,
772-                                   expiration_enabled, expiration_mode,
773-                                   expiration_override_lease_duration,
774-                                   expiration_cutoff_date,
775-                                   expiration_sharetypes)
776-        self.lease_checker.setServiceParent(self)
777 
778     def __repr__(self):
779         return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
780hunk ./src/allmydata/storage/server.py 52
781 
782-    def add_bucket_counter(self):
783-        statefile = os.path.join(self.storedir, "bucket_counter.state")
784-        self.bucket_counter = BucketCountingCrawler(self, statefile)
785-        self.bucket_counter.setServiceParent(self)
786-
787     def count(self, name, delta=1):
788         if self.stats_provider:
789             self.stats_provider.count("storage_server." + name, delta)
790hunk ./src/allmydata/storage/server.py 66
791         """Return a dict, indexed by category, that contains a dict of
792         latency numbers for each category. If there are sufficient samples
793         for unambiguous interpretation, each dict will contain the
794-        following keys: mean, 01_0_percentile, 10_0_percentile,
795+        following keys: samplesize, mean, 01_0_percentile, 10_0_percentile,
796         50_0_percentile (median), 90_0_percentile, 95_0_percentile,
797         99_0_percentile, 99_9_percentile.  If there are insufficient
798         samples for a given percentile to be interpreted unambiguously
799hunk ./src/allmydata/storage/server.py 88
800             else:
801                 stats["mean"] = None
802 
803-            orderstatlist = [(0.01, "01_0_percentile", 100), (0.1, "10_0_percentile", 10),\
804-                             (0.50, "50_0_percentile", 10), (0.90, "90_0_percentile", 10),\
805-                             (0.95, "95_0_percentile", 20), (0.99, "99_0_percentile", 100),\
806+            orderstatlist = [(0.1, "10_0_percentile", 10), (0.5, "50_0_percentile", 10), \
807+                             (0.9, "90_0_percentile", 10), (0.95, "95_0_percentile", 20), \
808+                             (0.01, "01_0_percentile", 100),  (0.99, "99_0_percentile", 100),\
809                              (0.999, "99_9_percentile", 1000)]
810 
811             for percentile, percentilestring, minnumtoobserve in orderstatlist:
812hunk ./src/allmydata/storage/server.py 107
813             kwargs["facility"] = "tahoe.storage"
814         return log.msg(*args, **kwargs)
815 
816-    def _clean_incomplete(self):
817-        fileutil.rm_dir(self.incomingdir)
818-
819     def get_stats(self):
820         # remember: RIStatsProvider requires that our return dict
821hunk ./src/allmydata/storage/server.py 109
822-        # contains numeric values.
823+        # contains numeric, or None values.
824         stats = { 'storage_server.allocated': self.allocated_size(), }
825         stats['storage_server.reserved_space'] = self.reserved_space
826         for category,ld in self.get_latencies().items():
827merger 0.0 (
828hunk ./src/allmydata/storage/server.py 149
829-        return fileutil.get_available_space(self.storedir, self.reserved_space)
830+        return fileutil.get_available_space(self.sharedir, self.reserved_space)
831hunk ./src/allmydata/storage/server.py 143
832-    def get_available_space(self):
833-        """Returns available space for share storage in bytes, or None if no
834-        API to get this information is available."""
835-
836-        if self.readonly_storage:
837-            return 0
838-        return fileutil.get_available_space(self.storedir, self.reserved_space)
839-
840)
841hunk ./src/allmydata/storage/server.py 158
842         return space
843 
844     def remote_get_version(self):
845-        remaining_space = self.get_available_space()
846+        remaining_space = self.backend.get_available_space()
847         if remaining_space is None:
848             # We're on a platform that has no API to get disk stats.
849             remaining_space = 2**64
850hunk ./src/allmydata/storage/server.py 172
851                     }
852         return version
853 
854-    def remote_allocate_buckets(self, storage_index,
855+    def remote_allocate_buckets(self, storageindex,
856                                 renew_secret, cancel_secret,
857                                 sharenums, allocated_size,
858                                 canary, owner_num=0):
859hunk ./src/allmydata/storage/server.py 181
860         # to a particular owner.
861         start = time.time()
862         self.count("allocate")
863-        alreadygot = set()
864+        incoming = set()
865         bucketwriters = {} # k: shnum, v: BucketWriter
866hunk ./src/allmydata/storage/server.py 183
867-        si_dir = storage_index_to_dir(storage_index)
868-        si_s = si_b2a(storage_index)
869 
870hunk ./src/allmydata/storage/server.py 184
871+        si_s = si_b2a(storageindex)
872         log.msg("storage: allocate_buckets %s" % si_s)
873 
874         # in this implementation, the lease information (including secrets)
875hunk ./src/allmydata/storage/server.py 198
876 
877         max_space_per_bucket = allocated_size
878 
879-        remaining_space = self.get_available_space()
880+        remaining_space = self.backend.get_available_space()
881         limited = remaining_space is not None
882         if limited:
883             # this is a bit conservative, since some of this allocated_size()
884hunk ./src/allmydata/storage/server.py 207
885             remaining_space -= self.allocated_size()
886         # self.readonly_storage causes remaining_space <= 0
887 
888-        # fill alreadygot with all shares that we have, not just the ones
889+        # Fill alreadygot with all shares that we have, not just the ones
890         # they asked about: this will save them a lot of work. Add or update
891         # leases for all of them: if they want us to hold shares for this
892hunk ./src/allmydata/storage/server.py 210
893-        # file, they'll want us to hold leases for this file.
894-        for (shnum, fn) in self._get_bucket_shares(storage_index):
895-            alreadygot.add(shnum)
896-            sf = ShareFile(fn)
897-            sf.add_or_renew_lease(lease_info)
898+        # file, they'll want us to hold leases for all the shares of it.
899+        alreadygot = set()
900+        for share in self.backend.get_shares(storageindex):
901+            share.add_or_renew_lease(lease_info)
902+            alreadygot.add(share.shnum)
903 
904hunk ./src/allmydata/storage/server.py 216
905-        for shnum in sharenums:
906-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
907-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
908-            if os.path.exists(finalhome):
909-                # great! we already have it. easy.
910-                pass
911-            elif os.path.exists(incominghome):
912-                # Note that we don't create BucketWriters for shnums that
913-                # have a partial share (in incoming/), so if a second upload
914-                # occurs while the first is still in progress, the second
915-                # uploader will use different storage servers.
916-                pass
917-            elif (not limited) or (remaining_space >= max_space_per_bucket):
918-                # ok! we need to create the new share file.
919-                bw = BucketWriter(self, incominghome, finalhome,
920-                                  max_space_per_bucket, lease_info, canary)
921-                if self.no_storage:
922-                    bw.throw_out_all_data = True
923+        # all share numbers that are incoming
924+        incoming = self.backend.get_incoming_shnums(storageindex)
925+
926+        for shnum in ((sharenums - alreadygot) - incoming):
927+            if (not limited) or (remaining_space >= max_space_per_bucket):
928+                bw = self.backend.make_bucket_writer(storageindex, shnum, max_space_per_bucket, lease_info, canary)
929                 bucketwriters[shnum] = bw
930                 self._active_writers[bw] = 1
931                 if limited:
932hunk ./src/allmydata/storage/server.py 227
933                     remaining_space -= max_space_per_bucket
934             else:
935-                # bummer! not enough space to accept this bucket
936+                # Bummer not enough space to accept this share.
937                 pass
938 
939hunk ./src/allmydata/storage/server.py 230
940-        if bucketwriters:
941-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
942-
943         self.add_latency("allocate", time.time() - start)
944         return alreadygot, bucketwriters
945 
946hunk ./src/allmydata/storage/server.py 233
947-    def _iter_share_files(self, storage_index):
948-        for shnum, filename in self._get_bucket_shares(storage_index):
949+    def _iter_share_files(self, storageindex):
950+        for shnum, filename in self._get_shares(storageindex):
951             f = open(filename, 'rb')
952             header = f.read(32)
953             f.close()
954hunk ./src/allmydata/storage/server.py 239
955             if header[:32] == MutableShareFile.MAGIC:
956+                # XXX  Can I exploit this code?
957                 sf = MutableShareFile(filename, self)
958                 # note: if the share has been migrated, the renew_lease()
959                 # call will throw an exception, with information to help the
960hunk ./src/allmydata/storage/server.py 245
961                 # client update the lease.
962             elif header[:4] == struct.pack(">L", 1):
963+                # Check if version number is "1".
964+                # XXX WHAT ABOUT OTHER VERSIONS!!!!!!!?
965                 sf = ShareFile(filename)
966             else:
967                 continue # non-sharefile
968hunk ./src/allmydata/storage/server.py 252
969             yield sf
970 
971-    def remote_add_lease(self, storage_index, renew_secret, cancel_secret,
972+    def remote_add_lease(self, storageindex, renew_secret, cancel_secret,
973                          owner_num=1):
974         start = time.time()
975         self.count("add-lease")
976hunk ./src/allmydata/storage/server.py 260
977         lease_info = LeaseInfo(owner_num,
978                                renew_secret, cancel_secret,
979                                new_expire_time, self.my_nodeid)
980-        for sf in self._iter_share_files(storage_index):
981+        for sf in self._iter_share_files(storageindex):
982             sf.add_or_renew_lease(lease_info)
983         self.add_latency("add-lease", time.time() - start)
984         return None
985hunk ./src/allmydata/storage/server.py 265
986 
987-    def remote_renew_lease(self, storage_index, renew_secret):
988+    def remote_renew_lease(self, storageindex, renew_secret):
989         start = time.time()
990         self.count("renew")
991         new_expire_time = time.time() + 31*24*60*60
992hunk ./src/allmydata/storage/server.py 270
993         found_buckets = False
994-        for sf in self._iter_share_files(storage_index):
995+        for sf in self._iter_share_files(storageindex):
996             found_buckets = True
997             sf.renew_lease(renew_secret, new_expire_time)
998         self.add_latency("renew", time.time() - start)
999hunk ./src/allmydata/storage/server.py 277
1000         if not found_buckets:
1001             raise IndexError("no such lease to renew")
1002 
1003-    def remote_cancel_lease(self, storage_index, cancel_secret):
1004+    def remote_cancel_lease(self, storageindex, cancel_secret):
1005         start = time.time()
1006         self.count("cancel")
1007 
1008hunk ./src/allmydata/storage/server.py 283
1009         total_space_freed = 0
1010         found_buckets = False
1011-        for sf in self._iter_share_files(storage_index):
1012+        for sf in self._iter_share_files(storageindex):
1013             # note: if we can't find a lease on one share, we won't bother
1014             # looking in the others. Unless something broke internally
1015             # (perhaps we ran out of disk space while adding a lease), the
1016hunk ./src/allmydata/storage/server.py 293
1017             total_space_freed += sf.cancel_lease(cancel_secret)
1018 
1019         if found_buckets:
1020-            storagedir = os.path.join(self.sharedir,
1021-                                      storage_index_to_dir(storage_index))
1022-            if not os.listdir(storagedir):
1023-                os.rmdir(storagedir)
1024+            # XXX  Yikes looks like code that shouldn't be in the server!
1025+            storagedir = si_si2dir(self.sharedir, storageindex)
1026+            fp_rmdir_if_empty(storagedir)
1027 
1028         if self.stats_provider:
1029             self.stats_provider.count('storage_server.bytes_freed',
1030hunk ./src/allmydata/storage/server.py 309
1031             self.stats_provider.count('storage_server.bytes_added', consumed_size)
1032         del self._active_writers[bw]
1033 
1034-    def _get_bucket_shares(self, storage_index):
1035-        """Return a list of (shnum, pathname) tuples for files that hold
1036-        shares for this storage_index. In each tuple, 'shnum' will always be
1037-        the integer form of the last component of 'pathname'."""
1038-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
1039-        try:
1040-            for f in os.listdir(storagedir):
1041-                if NUM_RE.match(f):
1042-                    filename = os.path.join(storagedir, f)
1043-                    yield (int(f), filename)
1044-        except OSError:
1045-            # Commonly caused by there being no buckets at all.
1046-            pass
1047-
1048-    def remote_get_buckets(self, storage_index):
1049+    def remote_get_buckets(self, storageindex):
1050         start = time.time()
1051         self.count("get")
1052hunk ./src/allmydata/storage/server.py 312
1053-        si_s = si_b2a(storage_index)
1054+        si_s = si_b2a(storageindex)
1055         log.msg("storage: get_buckets %s" % si_s)
1056         bucketreaders = {} # k: sharenum, v: BucketReader
1057hunk ./src/allmydata/storage/server.py 315
1058-        for shnum, filename in self._get_bucket_shares(storage_index):
1059-            bucketreaders[shnum] = BucketReader(self, filename,
1060-                                                storage_index, shnum)
1061+        self.backend.set_storage_server(self)
1062+        for share in self.backend.get_shares(storageindex):
1063+            bucketreaders[share.get_shnum()] = self.backend.make_bucket_reader(share)
1064         self.add_latency("get", time.time() - start)
1065         return bucketreaders
1066 
1067hunk ./src/allmydata/storage/server.py 321
1068-    def get_leases(self, storage_index):
1069+    def get_leases(self, storageindex):
1070         """Provide an iterator that yields all of the leases attached to this
1071         bucket. Each lease is returned as a LeaseInfo instance.
1072 
1073hunk ./src/allmydata/storage/server.py 331
1074         # since all shares get the same lease data, we just grab the leases
1075         # from the first share
1076         try:
1077-            shnum, filename = self._get_bucket_shares(storage_index).next()
1078+            shnum, filename = self._get_shares(storageindex).next()
1079             sf = ShareFile(filename)
1080             return sf.get_leases()
1081         except StopIteration:
1082hunk ./src/allmydata/storage/server.py 337
1083             return iter([])
1084 
1085-    def remote_slot_testv_and_readv_and_writev(self, storage_index,
1086+    #  XXX  As far as Zancas' grockery has gotten.
1087+    def remote_slot_testv_and_readv_and_writev(self, storageindex,
1088                                                secrets,
1089                                                test_and_write_vectors,
1090                                                read_vector):
1091hunk ./src/allmydata/storage/server.py 344
1092         start = time.time()
1093         self.count("writev")
1094-        si_s = si_b2a(storage_index)
1095+        si_s = si_b2a(storageindex)
1096         log.msg("storage: slot_writev %s" % si_s)
1097hunk ./src/allmydata/storage/server.py 346
1098-        si_dir = storage_index_to_dir(storage_index)
1099+       
1100         (write_enabler, renew_secret, cancel_secret) = secrets
1101         # shares exist if there is a file for them
1102hunk ./src/allmydata/storage/server.py 349
1103-        bucketdir = os.path.join(self.sharedir, si_dir)
1104+        bucketdir = si_si2dir(self.sharedir, storageindex)
1105         shares = {}
1106         if os.path.isdir(bucketdir):
1107             for sharenum_s in os.listdir(bucketdir):
1108hunk ./src/allmydata/storage/server.py 432
1109                                          self)
1110         return share
1111 
1112-    def remote_slot_readv(self, storage_index, shares, readv):
1113+    def remote_slot_readv(self, storageindex, shares, readv):
1114         start = time.time()
1115         self.count("readv")
1116hunk ./src/allmydata/storage/server.py 435
1117-        si_s = si_b2a(storage_index)
1118+        si_s = si_b2a(storageindex)
1119         lp = log.msg("storage: slot_readv %s %s" % (si_s, shares),
1120                      facility="tahoe.storage", level=log.OPERATIONAL)
1121hunk ./src/allmydata/storage/server.py 438
1122-        si_dir = storage_index_to_dir(storage_index)
1123         # shares exist if there is a file for them
1124hunk ./src/allmydata/storage/server.py 439
1125-        bucketdir = os.path.join(self.sharedir, si_dir)
1126+        bucketdir = si_si2dir(self.sharedir, storageindex)
1127         if not os.path.isdir(bucketdir):
1128             self.add_latency("readv", time.time() - start)
1129             return {}
1130hunk ./src/allmydata/storage/server.py 458
1131         self.add_latency("readv", time.time() - start)
1132         return datavs
1133 
1134-    def remote_advise_corrupt_share(self, share_type, storage_index, shnum,
1135+    def remote_advise_corrupt_share(self, share_type, storageindex, shnum,
1136                                     reason):
1137         fileutil.make_dirs(self.corruption_advisory_dir)
1138         now = time_format.iso_utc(sep="T")
1139hunk ./src/allmydata/storage/server.py 462
1140-        si_s = si_b2a(storage_index)
1141+        si_s = si_b2a(storageindex)
1142         # windows can't handle colons in the filename
1143         fn = os.path.join(self.corruption_advisory_dir,
1144                           "%s--%s-%d" % (now, si_s, shnum)).replace(":","")
1145hunk ./src/allmydata/storage/server.py 469
1146         f = open(fn, "w")
1147         f.write("report: Share Corruption\n")
1148         f.write("type: %s\n" % share_type)
1149-        f.write("storage_index: %s\n" % si_s)
1150+        f.write("storageindex: %s\n" % si_s)
1151         f.write("share_number: %d\n" % shnum)
1152         f.write("\n")
1153         f.write(reason)
1154}
1155[modify null/core.py such that the correct interfaces are implemented
1156wilcoxjg@gmail.com**20110809201822
1157 Ignore-this: 3c64580592474f71633287d1b6beeb6b
1158] hunk ./src/allmydata/storage/backends/null/core.py 4
1159 from allmydata.storage.backends.base import Backend
1160 from allmydata.storage.immutable import BucketWriter, BucketReader
1161 from zope.interface import implements
1162+from allmydata.interfaces import IStorageBackend, IStorageBackendShare
1163 
1164 class NullCore(Backend):
1165     implements(IStorageBackend)
1166[make changes to storage/immutable.py most changes are part of movement to DAS specific backend.
1167wilcoxjg@gmail.com**20110809202232
1168 Ignore-this: 70c7c6ea6be2418d70556718a050714
1169] {
1170hunk ./src/allmydata/storage/immutable.py 1
1171-import os, stat, struct, time
1172+import os, time
1173 
1174 from foolscap.api import Referenceable
1175 
1176hunk ./src/allmydata/storage/immutable.py 7
1177 from zope.interface import implements
1178 from allmydata.interfaces import RIBucketWriter, RIBucketReader
1179-from allmydata.util import base32, fileutil, log
1180+from allmydata.util import base32, log
1181 from allmydata.util.assertutil import precondition
1182 from allmydata.util.hashutil import constant_time_compare
1183 from allmydata.storage.lease import LeaseInfo
1184hunk ./src/allmydata/storage/immutable.py 14
1185 from allmydata.storage.common import UnknownImmutableContainerVersionError, \
1186      DataTooLargeError
1187 
1188-# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1189-# and share data. The share data is accessed by RIBucketWriter.write and
1190-# RIBucketReader.read . The lease information is not accessible through these
1191-# interfaces.
1192-
1193-# The share file has the following layout:
1194-#  0x00: share file version number, four bytes, current version is 1
1195-#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1196-#  0x08: number of leases, four bytes big-endian
1197-#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1198-#  A+0x0c = B: first lease. Lease format is:
1199-#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1200-#   B+0x04: renew secret, 32 bytes (SHA256)
1201-#   B+0x24: cancel secret, 32 bytes (SHA256)
1202-#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1203-#   B+0x48: next lease, or end of record
1204-
1205-# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1206-# but it is still filled in by storage servers in case the storage server
1207-# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1208-# share file is moved from one storage server to another. The value stored in
1209-# this field is truncated, so if the actual share data length is >= 2**32,
1210-# then the value stored in this field will be the actual share data length
1211-# modulo 2**32.
1212-
1213-class ShareFile:
1214-    LEASE_SIZE = struct.calcsize(">L32s32sL")
1215-    sharetype = "immutable"
1216-
1217-    def __init__(self, filename, max_size=None, create=False):
1218-        """ 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. """
1219-        precondition((max_size is not None) or (not create), max_size, create)
1220-        self.home = filename
1221-        self._max_size = max_size
1222-        if create:
1223-            # touch the file, so later callers will see that we're working on
1224-            # it. Also construct the metadata.
1225-            assert not os.path.exists(self.home)
1226-            fileutil.make_dirs(os.path.dirname(self.home))
1227-            f = open(self.home, 'wb')
1228-            # The second field -- the four-byte share data length -- is no
1229-            # longer used as of Tahoe v1.3.0, but we continue to write it in
1230-            # there in case someone downgrades a storage server from >=
1231-            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1232-            # server to another, etc. We do saturation -- a share data length
1233-            # larger than 2**32-1 (what can fit into the field) is marked as
1234-            # the largest length that can fit into the field. That way, even
1235-            # if this does happen, the old < v1.3.0 server will still allow
1236-            # clients to read the first part of the share.
1237-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1238-            f.close()
1239-            self._lease_offset = max_size + 0x0c
1240-            self._num_leases = 0
1241-        else:
1242-            f = open(self.home, 'rb')
1243-            filesize = os.path.getsize(self.home)
1244-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1245-            f.close()
1246-            if version != 1:
1247-                msg = "sharefile %s had version %d but we wanted 1" % \
1248-                      (filename, version)
1249-                raise UnknownImmutableContainerVersionError(msg)
1250-            self._num_leases = num_leases
1251-            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1252-        self._data_offset = 0xc
1253-
1254-    def unlink(self):
1255-        os.unlink(self.home)
1256-
1257-    def read_share_data(self, offset, length):
1258-        precondition(offset >= 0)
1259-        # reads beyond the end of the data are truncated. Reads that start
1260-        # beyond the end of the data return an empty string. I wonder why
1261-        # Python doesn't do the following computation for me?
1262-        seekpos = self._data_offset+offset
1263-        fsize = os.path.getsize(self.home)
1264-        actuallength = max(0, min(length, fsize-seekpos))
1265-        if actuallength == 0:
1266-            return ""
1267-        f = open(self.home, 'rb')
1268-        f.seek(seekpos)
1269-        return f.read(actuallength)
1270-
1271-    def write_share_data(self, offset, data):
1272-        length = len(data)
1273-        precondition(offset >= 0, offset)
1274-        if self._max_size is not None and offset+length > self._max_size:
1275-            raise DataTooLargeError(self._max_size, offset, length)
1276-        f = open(self.home, 'rb+')
1277-        real_offset = self._data_offset+offset
1278-        f.seek(real_offset)
1279-        assert f.tell() == real_offset
1280-        f.write(data)
1281-        f.close()
1282-
1283-    def _write_lease_record(self, f, lease_number, lease_info):
1284-        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1285-        f.seek(offset)
1286-        assert f.tell() == offset
1287-        f.write(lease_info.to_immutable_data())
1288-
1289-    def _read_num_leases(self, f):
1290-        f.seek(0x08)
1291-        (num_leases,) = struct.unpack(">L", f.read(4))
1292-        return num_leases
1293-
1294-    def _write_num_leases(self, f, num_leases):
1295-        f.seek(0x08)
1296-        f.write(struct.pack(">L", num_leases))
1297-
1298-    def _truncate_leases(self, f, num_leases):
1299-        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1300-
1301-    def get_leases(self):
1302-        """Yields a LeaseInfo instance for all leases."""
1303-        f = open(self.home, 'rb')
1304-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1305-        f.seek(self._lease_offset)
1306-        for i in range(num_leases):
1307-            data = f.read(self.LEASE_SIZE)
1308-            if data:
1309-                yield LeaseInfo().from_immutable_data(data)
1310-
1311-    def add_lease(self, lease_info):
1312-        f = open(self.home, 'rb+')
1313-        num_leases = self._read_num_leases(f)
1314-        self._write_lease_record(f, num_leases, lease_info)
1315-        self._write_num_leases(f, num_leases+1)
1316-        f.close()
1317-
1318-    def renew_lease(self, renew_secret, new_expire_time):
1319-        for i,lease in enumerate(self.get_leases()):
1320-            if constant_time_compare(lease.renew_secret, renew_secret):
1321-                # yup. See if we need to update the owner time.
1322-                if new_expire_time > lease.expiration_time:
1323-                    # yes
1324-                    lease.expiration_time = new_expire_time
1325-                    f = open(self.home, 'rb+')
1326-                    self._write_lease_record(f, i, lease)
1327-                    f.close()
1328-                return
1329-        raise IndexError("unable to renew non-existent lease")
1330-
1331-    def add_or_renew_lease(self, lease_info):
1332-        try:
1333-            self.renew_lease(lease_info.renew_secret,
1334-                             lease_info.expiration_time)
1335-        except IndexError:
1336-            self.add_lease(lease_info)
1337-
1338-
1339-    def cancel_lease(self, cancel_secret):
1340-        """Remove a lease with the given cancel_secret. If the last lease is
1341-        cancelled, the file will be removed. Return the number of bytes that
1342-        were freed (by truncating the list of leases, and possibly by
1343-        deleting the file. Raise IndexError if there was no lease with the
1344-        given cancel_secret.
1345-        """
1346-
1347-        leases = list(self.get_leases())
1348-        num_leases_removed = 0
1349-        for i,lease in enumerate(leases):
1350-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1351-                leases[i] = None
1352-                num_leases_removed += 1
1353-        if not num_leases_removed:
1354-            raise IndexError("unable to find matching lease to cancel")
1355-        if num_leases_removed:
1356-            # pack and write out the remaining leases. We write these out in
1357-            # the same order as they were added, so that if we crash while
1358-            # doing this, we won't lose any non-cancelled leases.
1359-            leases = [l for l in leases if l] # remove the cancelled leases
1360-            f = open(self.home, 'rb+')
1361-            for i,lease in enumerate(leases):
1362-                self._write_lease_record(f, i, lease)
1363-            self._write_num_leases(f, len(leases))
1364-            self._truncate_leases(f, len(leases))
1365-            f.close()
1366-        space_freed = self.LEASE_SIZE * num_leases_removed
1367-        if not len(leases):
1368-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1369-            self.unlink()
1370-        return space_freed
1371-
1372-
1373 class BucketWriter(Referenceable):
1374     implements(RIBucketWriter)
1375 
1376hunk ./src/allmydata/storage/immutable.py 17
1377-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1378+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
1379         self.ss = ss
1380hunk ./src/allmydata/storage/immutable.py 19
1381-        self.incominghome = incominghome
1382-        self.finalhome = finalhome
1383-        self._max_size = max_size # don't allow the client to write more than this
1384+        self._max_size = max_size # don't allow the client to write more than this        print self.ss._active_writers.keys()
1385         self._canary = canary
1386         self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1387         self.closed = False
1388hunk ./src/allmydata/storage/immutable.py 24
1389         self.throw_out_all_data = False
1390-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1391+        self._sharefile = immutableshare
1392         # also, add our lease to the file now, so that other ones can be
1393         # added by simultaneous uploaders
1394         self._sharefile.add_lease(lease_info)
1395hunk ./src/allmydata/storage/immutable.py 45
1396         precondition(not self.closed)
1397         start = time.time()
1398 
1399-        fileutil.make_dirs(os.path.dirname(self.finalhome))
1400-        fileutil.rename(self.incominghome, self.finalhome)
1401-        try:
1402-            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1403-            # We try to delete the parent (.../ab/abcde) to avoid leaving
1404-            # these directories lying around forever, but the delete might
1405-            # fail if we're working on another share for the same storage
1406-            # index (like ab/abcde/5). The alternative approach would be to
1407-            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1408-            # ShareWriter), each of which is responsible for a single
1409-            # directory on disk, and have them use reference counting of
1410-            # their children to know when they should do the rmdir. This
1411-            # approach is simpler, but relies on os.rmdir refusing to delete
1412-            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1413-            os.rmdir(os.path.dirname(self.incominghome))
1414-            # we also delete the grandparent (prefix) directory, .../ab ,
1415-            # again to avoid leaving directories lying around. This might
1416-            # fail if there is another bucket open that shares a prefix (like
1417-            # ab/abfff).
1418-            os.rmdir(os.path.dirname(os.path.dirname(self.incominghome)))
1419-            # we leave the great-grandparent (incoming/) directory in place.
1420-        except EnvironmentError:
1421-            # ignore the "can't rmdir because the directory is not empty"
1422-            # exceptions, those are normal consequences of the
1423-            # above-mentioned conditions.
1424-            pass
1425+        self._sharefile.close()
1426+        filelen = self._sharefile.stat()
1427         self._sharefile = None
1428hunk ./src/allmydata/storage/immutable.py 48
1429+
1430         self.closed = True
1431         self._canary.dontNotifyOnDisconnect(self._disconnect_marker)
1432 
1433hunk ./src/allmydata/storage/immutable.py 52
1434-        filelen = os.stat(self.finalhome)[stat.ST_SIZE]
1435         self.ss.bucket_writer_closed(self, filelen)
1436         self.ss.add_latency("close", time.time() - start)
1437         self.ss.count("close")
1438hunk ./src/allmydata/storage/immutable.py 90
1439 class BucketReader(Referenceable):
1440     implements(RIBucketReader)
1441 
1442-    def __init__(self, ss, sharefname, storage_index=None, shnum=None):
1443+    def __init__(self, ss, share):
1444         self.ss = ss
1445hunk ./src/allmydata/storage/immutable.py 92
1446-        self._share_file = ShareFile(sharefname)
1447-        self.storage_index = storage_index
1448-        self.shnum = shnum
1449+        self._share_file = share
1450+        self.storageindex = share.storageindex
1451+        self.shnum = share.shnum
1452 
1453     def __repr__(self):
1454         return "<%s %s %s>" % (self.__class__.__name__,
1455hunk ./src/allmydata/storage/immutable.py 98
1456-                               base32.b2a_l(self.storage_index[:8], 60),
1457+                               base32.b2a_l(self.storageindex[:8], 60),
1458                                self.shnum)
1459 
1460     def remote_read(self, offset, length):
1461hunk ./src/allmydata/storage/immutable.py 110
1462 
1463     def remote_advise_corrupt_share(self, reason):
1464         return self.ss.remote_advise_corrupt_share("immutable",
1465-                                                   self.storage_index,
1466+                                                   self.storageindex,
1467                                                    self.shnum,
1468                                                    reason)
1469}
1470[creates backends/das/core.py
1471wilcoxjg@gmail.com**20110809202620
1472 Ignore-this: 2ea937f8d02aa85396135903be91ed67
1473] {
1474adddir ./src/allmydata/storage/backends/das
1475addfile ./src/allmydata/storage/backends/das/core.py
1476hunk ./src/allmydata/storage/backends/das/core.py 1
1477+import re, weakref, struct, time, stat
1478+from twisted.application import service
1479+from twisted.python.filepath import UnlistableError
1480+from twisted.python import filepath
1481+from twisted.python.filepath import FilePath
1482+from zope.interface import implements
1483+
1484+import allmydata # for __full_version__
1485+from allmydata.interfaces import IStorageBackend
1486+from allmydata.storage.backends.base import Backend
1487+from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
1488+from allmydata.util.assertutil import precondition
1489+from allmydata.interfaces import IStatsProducer, IShareStore# XXX, RIStorageServer
1490+from allmydata.util import fileutil, idlib, log, time_format
1491+from allmydata.util.fileutil import fp_make_dirs
1492+from allmydata.storage.lease import LeaseInfo
1493+from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
1494+     create_mutable_sharefile
1495+from allmydata.storage.immutable import BucketWriter, BucketReader
1496+from allmydata.storage.crawler import BucketCountingCrawler
1497+from allmydata.util.hashutil import constant_time_compare
1498+from allmydata.storage.backends.das.expirer import LeaseCheckingCrawler
1499+_pyflakes_hush = [si_b2a, si_a2b, si_si2dir] # re-exported
1500+
1501+# storage/
1502+# storage/shares/incoming
1503+#   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
1504+#   be moved to storage/shares/$START/$STORAGEINDEX/$SHARENUM upon success
1505+# storage/shares/$START/$STORAGEINDEX
1506+# storage/shares/$START/$STORAGEINDEX/$SHARENUM
1507+
1508+# Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
1509+# base-32 chars).
1510+# $SHARENUM matches this regex:
1511+NUM_RE=re.compile("^[0-9]+$")
1512+
1513+class DASCore(Backend):
1514+    implements(IStorageBackend)
1515+    def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
1516+        Backend.__init__(self)
1517+        self._setup_storage(storedir, readonly, reserved_space)
1518+        self._setup_corruption_advisory()
1519+        self._setup_bucket_counter()
1520+        self._setup_lease_checkerf(expiration_policy)
1521+
1522+    def _setup_storage(self, storedir, readonly, reserved_space):
1523+        precondition(isinstance(storedir, FilePath), storedir, FilePath) 
1524+        self.storedir = storedir
1525+        self.readonly = readonly
1526+        self.reserved_space = int(reserved_space)
1527+        self.sharedir = self.storedir.child("shares")
1528+        fileutil.fp_make_dirs(self.sharedir)
1529+        self.incomingdir = self.sharedir.child('incoming')
1530+        self._clean_incomplete()
1531+        if self.reserved_space and (self.get_available_space() is None):
1532+            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",
1533+                    umid="0wZ27w", level=log.UNUSUAL)
1534+
1535+
1536+    def _clean_incomplete(self):
1537+        fileutil.fp_remove(self.incomingdir)
1538+        fileutil.fp_make_dirs(self.incomingdir)
1539+
1540+    def _setup_corruption_advisory(self):
1541+        # we don't actually create the corruption-advisory dir until necessary
1542+        self.corruption_advisory_dir = self.storedir.child("corruption-advisories")
1543+
1544+    def _setup_bucket_counter(self):
1545+        statefname = self.storedir.child("bucket_counter.state")
1546+        self.bucket_counter = BucketCountingCrawler(statefname)
1547+        self.bucket_counter.setServiceParent(self)
1548+
1549+    def _setup_lease_checkerf(self, expiration_policy):
1550+        statefile = self.storedir.child("lease_checker.state")
1551+        historyfile = self.storedir.child("lease_checker.history")
1552+        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile, expiration_policy)
1553+        self.lease_checker.setServiceParent(self)
1554+
1555+    def get_incoming_shnums(self, storageindex):
1556+        """ Return a frozenset of the shnum (as ints) of incoming shares. """
1557+        incomingthissi = si_si2dir(self.incomingdir, storageindex)
1558+        try:
1559+            childfps = [ fp for fp in incomingthissi.children() if NUM_RE.match(fp.basename()) ]
1560+            shnums = [ int(fp.basename()) for fp in childfps]
1561+            return frozenset(shnums)
1562+        except UnlistableError:
1563+            # There is no shares directory at all.
1564+            return frozenset()
1565+           
1566+    def get_shares(self, storageindex):
1567+        """ Generate ImmutableShare objects for shares we have for this
1568+        storageindex. ("Shares we have" means completed ones, excluding
1569+        incoming ones.)"""
1570+        finalstoragedir = si_si2dir(self.sharedir, storageindex)
1571+        try:
1572+            for fp in finalstoragedir.children():
1573+                fpshnumstr = fp.basename()
1574+                if NUM_RE.match(fpshnumstr):
1575+                    finalhome = finalstoragedir.child(fpshnumstr)
1576+                    yield ImmutableShare(storageindex, fpshnumstr, finalhome)
1577+        except UnlistableError:
1578+            # There is no shares directory at all.
1579+            pass
1580+       
1581+    def get_available_space(self):
1582+        if self.readonly:
1583+            return 0
1584+        return fileutil.get_available_space(self.storedir, self.reserved_space)
1585+
1586+    def make_bucket_writer(self, storageindex, shnum, max_space_per_bucket, lease_info, canary):
1587+        finalhome = si_si2dir(self.sharedir, storageindex).child(str(shnum))
1588+        incominghome = si_si2dir(self.incomingdir, storageindex).child(str(shnum))
1589+        immsh = ImmutableShare(storageindex, shnum, finalhome, incominghome, max_size=max_space_per_bucket, create=True)
1590+        bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1591+        return bw
1592+
1593+    def make_bucket_reader(self, share):
1594+        return BucketReader(self.ss, share)
1595+
1596+    def set_storage_server(self, ss):
1597+        self.ss = ss
1598+       
1599+
1600+# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1601+# and share data. The share data is accessed by RIBucketWriter.write and
1602+# RIBucketReader.read . The lease information is not accessible through these
1603+# interfaces.
1604+
1605+# The share file has the following layout:
1606+#  0x00: share file version number, four bytes, current version is 1
1607+#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1608+#  0x08: number of leases, four bytes big-endian
1609+#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1610+#  A+0x0c = B: first lease. Lease format is:
1611+#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1612+#   B+0x04: renew secret, 32 bytes (SHA256)
1613+#   B+0x24: cancel secret, 32 bytes (SHA256)
1614+#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1615+#   B+0x48: next lease, or end of record
1616+
1617+# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1618+# but it is still filled in by storage servers in case the storage server
1619+# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1620+# share file is moved from one storage server to another. The value stored in
1621+# this field is truncated, so if the actual share data length is >= 2**32,
1622+# then the value stored in this field will be the actual share data length
1623+# modulo 2**32.
1624+
1625+class ImmutableShare(object):
1626+    LEASE_SIZE = struct.calcsize(">L32s32sL")
1627+    sharetype = "immutable"
1628+
1629+    def __init__(self, storageindex, shnum, finalhome=None, incominghome=None, max_size=None, create=False):
1630+        """ If max_size is not None then I won't allow more than
1631+        max_size to be written to me. If create=True then max_size
1632+        must not be None. """
1633+        precondition((max_size is not None) or (not create), max_size, create)
1634+        self.storageindex = storageindex
1635+        self._max_size = max_size
1636+        self.incominghome = incominghome
1637+        self.finalhome = finalhome
1638+        self.shnum = shnum
1639+        if create:
1640+            # touch the file, so later callers will see that we're working on
1641+            # it. Also construct the metadata.
1642+            assert not finalhome.exists()
1643+            fp_make_dirs(self.incominghome.parent())
1644+            # The second field -- the four-byte share data length -- is no
1645+            # longer used as of Tahoe v1.3.0, but we continue to write it in
1646+            # there in case someone downgrades a storage server from >=
1647+            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1648+            # server to another, etc. We do saturation -- a share data length
1649+            # larger than 2**32-1 (what can fit into the field) is marked as
1650+            # the largest length that can fit into the field. That way, even
1651+            # if this does happen, the old < v1.3.0 server will still allow
1652+            # clients to read the first part of the share.
1653+            self.incominghome.setContent(struct.pack(">LLL", 1, min(2**32-1, max_size), 0) )
1654+            self._lease_offset = max_size + 0x0c
1655+            self._num_leases = 0
1656+        else:
1657+            fh = self.finalhome.open(mode='rb')
1658+            try:
1659+                (version, unused, num_leases) = struct.unpack(">LLL", fh.read(0xc))
1660+            finally:
1661+                fh.close()
1662+            filesize = self.finalhome.getsize()
1663+            if version != 1:
1664+                msg = "sharefile %s had version %d but we wanted 1" % \
1665+                      (self.finalhome, version)
1666+                raise UnknownImmutableContainerVersionError(msg)
1667+            self._num_leases = num_leases
1668+            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1669+        self._data_offset = 0xc
1670+
1671+    def close(self):
1672+        fileutil.fp_make_dirs(self.finalhome.parent())
1673+        self.incominghome.moveTo(self.finalhome)
1674+        try:
1675+            # self.incominghome is like storage/shares/incoming/ab/abcde/4 .
1676+            # We try to delete the parent (.../ab/abcde) to avoid leaving
1677+            # these directories lying around forever, but the delete might
1678+            # fail if we're working on another share for the same storage
1679+            # index (like ab/abcde/5). The alternative approach would be to
1680+            # use a hierarchy of objects (PrefixHolder, BucketHolder,
1681+            # ShareWriter), each of which is responsible for a single
1682+            # directory on disk, and have them use reference counting of
1683+            # their children to know when they should do the rmdir. This
1684+            # approach is simpler, but relies on os.rmdir refusing to delete
1685+            # a non-empty directory. Do *not* use fileutil.rm_dir() here!
1686+            fileutil.fp_rmdir_if_empty(self.incominghome.parent())
1687+            # we also delete the grandparent (prefix) directory, .../ab ,
1688+            # again to avoid leaving directories lying around. This might
1689+            # fail if there is another bucket open that shares a prefix (like
1690+            # ab/abfff).
1691+            fileutil.fp_rmdir_if_empty(self.incominghome.parent().parent())
1692+            # we leave the great-grandparent (incoming/) directory in place.
1693+        except EnvironmentError:
1694+            # ignore the "can't rmdir because the directory is not empty"
1695+            # exceptions, those are normal consequences of the
1696+            # above-mentioned conditions.
1697+            pass
1698+        pass
1699+       
1700+    def stat(self):
1701+        return filepath.stat(self.finalhome.path)[stat.ST_SIZE]
1702+
1703+    def get_shnum(self):
1704+        return self.shnum
1705+
1706+    def unlink(self):
1707+        self.finalhome.remove()
1708+
1709+    def read_share_data(self, offset, length):
1710+        precondition(offset >= 0)
1711+        # Reads beyond the end of the data are truncated. Reads that start
1712+        # beyond the end of the data return an empty string.
1713+        seekpos = self._data_offset+offset
1714+        fsize = self.finalhome.getsize()
1715+        actuallength = max(0, min(length, fsize-seekpos))
1716+        if actuallength == 0:
1717+            return ""
1718+        fh = self.finalhome.open(mode='rb')
1719+        try:
1720+            fh.seek(seekpos)
1721+            sharedata = fh.read(actuallength)
1722+        finally:
1723+            fh.close()
1724+        return sharedata
1725+
1726+    def write_share_data(self, offset, data):
1727+        length = len(data)
1728+        precondition(offset >= 0, offset)
1729+        if self._max_size is not None and offset+length > self._max_size:
1730+            raise DataTooLargeError(self._max_size, offset, length)
1731+        fh = self.incominghome.open(mode='rb+')
1732+        try:
1733+            real_offset = self._data_offset+offset
1734+            fh.seek(real_offset)
1735+            assert fh.tell() == real_offset
1736+            fh.write(data)
1737+        finally:
1738+            fh.close()
1739+
1740+    def _write_lease_record(self, f, lease_number, lease_info):
1741+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1742+        fh = f.open()
1743+        try:
1744+            fh.seek(offset)
1745+            assert fh.tell() == offset
1746+            fh.write(lease_info.to_immutable_data())
1747+        finally:
1748+            fh.close()
1749+
1750+    def _read_num_leases(self, f):
1751+        fh = f.open() #XXX  Should be mocking FilePath.open()
1752+        try:
1753+            fh.seek(0x08)
1754+            ro = fh.read(4)
1755+            (num_leases,) = struct.unpack(">L", ro)
1756+        finally:
1757+            fh.close()
1758+        return num_leases
1759+
1760+    def _write_num_leases(self, f, num_leases):
1761+        fh = f.open()
1762+        try:
1763+            fh.seek(0x08)
1764+            fh.write(struct.pack(">L", num_leases))
1765+        finally:
1766+            fh.close()
1767+
1768+    def _truncate_leases(self, f, num_leases):
1769+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1770+
1771+    def get_leases(self):
1772+        """Yields a LeaseInfo instance for all leases."""
1773+        fh = self.finalhome.open(mode='rb')
1774+        (version, unused, num_leases) = struct.unpack(">LLL", fh.read(0xc))
1775+        fh.seek(self._lease_offset)
1776+        for i in range(num_leases):
1777+            data = fh.read(self.LEASE_SIZE)
1778+            if data:
1779+                yield LeaseInfo().from_immutable_data(data)
1780+
1781+    def add_lease(self, lease_info):
1782+        num_leases = self._read_num_leases(self.incominghome)
1783+        self._write_lease_record(self.incominghome, num_leases, lease_info)
1784+        self._write_num_leases(self.incominghome, num_leases+1)
1785+       
1786+    def renew_lease(self, renew_secret, new_expire_time):
1787+        for i,lease in enumerate(self.get_leases()):
1788+            if constant_time_compare(lease.renew_secret, renew_secret):
1789+                # yup. See if we need to update the owner time.
1790+                if new_expire_time > lease.expiration_time:
1791+                    # yes
1792+                    lease.expiration_time = new_expire_time
1793+                    f = open(self.finalhome, 'rb+')
1794+                    self._write_lease_record(f, i, lease)
1795+                    f.close()
1796+                return
1797+        raise IndexError("unable to renew non-existent lease")
1798+
1799+    def add_or_renew_lease(self, lease_info):
1800+        try:
1801+            self.renew_lease(lease_info.renew_secret,
1802+                             lease_info.expiration_time)
1803+        except IndexError:
1804+            self.add_lease(lease_info)
1805+
1806+    def cancel_lease(self, cancel_secret):
1807+        """Remove a lease with the given cancel_secret. If the last lease is
1808+        cancelled, the file will be removed. Return the number of bytes that
1809+        were freed (by truncating the list of leases, and possibly by
1810+        deleting the file. Raise IndexError if there was no lease with the
1811+        given cancel_secret.
1812+        """
1813+
1814+        leases = list(self.get_leases())
1815+        num_leases_removed = 0
1816+        for i,lease in enumerate(leases):
1817+            if constant_time_compare(lease.cancel_secret, cancel_secret):
1818+                leases[i] = None
1819+                num_leases_removed += 1
1820+        if not num_leases_removed:
1821+            raise IndexError("unable to find matching lease to cancel")
1822+        if num_leases_removed:
1823+            # pack and write out the remaining leases. We write these out in
1824+            # the same order as they were added, so that if we crash while
1825+            # doing this, we won't lose any non-cancelled leases.
1826+            leases = [l for l in leases if l] # remove the cancelled leases
1827+            f = open(self.finalhome, 'rb+')
1828+            for i,lease in enumerate(leases):
1829+                self._write_lease_record(f, i, lease)
1830+            self._write_num_leases(f, len(leases))
1831+            self._truncate_leases(f, len(leases))
1832+            f.close()
1833+        space_freed = self.LEASE_SIZE * num_leases_removed
1834+        if not len(leases):
1835+            space_freed += os.stat(self.finalhome)[stat.ST_SIZE]
1836+            self.unlink()
1837+        return space_freed
1838}
1839[change backends/das/core.py to correct which interfaces are implemented
1840wilcoxjg@gmail.com**20110809203123
1841 Ignore-this: 7f9331a04b55f7feee4335abee011e14
1842] hunk ./src/allmydata/storage/backends/das/core.py 13
1843 from allmydata.storage.backends.base import Backend
1844 from allmydata.storage.common import si_b2a, si_a2b, si_si2dir
1845 from allmydata.util.assertutil import precondition
1846-from allmydata.interfaces import IStatsProducer, IShareStore# XXX, RIStorageServer
1847+from allmydata.interfaces import IStorageBackend
1848 from allmydata.util import fileutil, idlib, log, time_format
1849 from allmydata.util.fileutil import fp_make_dirs
1850 from allmydata.storage.lease import LeaseInfo
1851[util/fileutil.py now expects and manipulates twisted.python.filepath.FilePath objects
1852wilcoxjg@gmail.com**20110809203321
1853 Ignore-this: 12c8aa13424ed51a5df09b92a454627
1854] {
1855hunk ./src/allmydata/util/fileutil.py 5
1856 Futz with files like a pro.
1857 """
1858 
1859-import sys, exceptions, os, stat, tempfile, time, binascii
1860+import errno, sys, exceptions, os, stat, tempfile, time, binascii
1861+
1862+from allmydata.util.assertutil import precondition
1863 
1864 from twisted.python import log
1865hunk ./src/allmydata/util/fileutil.py 10
1866+from twisted.python.filepath import FilePath, UnlistableError
1867 
1868 from pycryptopp.cipher.aes import AES
1869 
1870hunk ./src/allmydata/util/fileutil.py 189
1871             raise tx
1872         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...
1873 
1874-def rm_dir(dirname):
1875+def fp_make_dirs(dirfp):
1876+    """
1877+    An idempotent version of FilePath.makedirs().  If the dir already
1878+    exists, do nothing and return without raising an exception.  If this
1879+    call creates the dir, return without raising an exception.  If there is
1880+    an error that prevents creation or if the directory gets deleted after
1881+    fp_make_dirs() creates it and before fp_make_dirs() checks that it
1882+    exists, raise an exception.
1883+    """
1884+    log.msg( "xxx 0 %s" % (dirfp,))
1885+    tx = None
1886+    try:
1887+        dirfp.makedirs()
1888+    except OSError, x:
1889+        tx = x
1890+
1891+    if not dirfp.isdir():
1892+        if tx:
1893+            raise tx
1894+        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...
1895+
1896+def fp_rmdir_if_empty(dirfp):
1897+    """ Remove the directory if it is empty. """
1898+    try:
1899+        os.rmdir(dirfp.path)
1900+    except OSError, e:
1901+        if e.errno != errno.ENOTEMPTY:
1902+            raise
1903+    else:
1904+        dirfp.changed()
1905+
1906+def rmtree(dirname):
1907     """
1908     A threadsafe and idempotent version of shutil.rmtree().  If the dir is
1909     already gone, do nothing and return without raising an exception.  If this
1910hunk ./src/allmydata/util/fileutil.py 239
1911             else:
1912                 remove(fullname)
1913         os.rmdir(dirname)
1914-    except Exception, le:
1915-        # Ignore "No such file or directory"
1916-        if (not isinstance(le, OSError)) or le.args[0] != 2:
1917+    except EnvironmentError, le:
1918+        # Ignore "No such file or directory", collect any other exception.
1919+        if (le.args[0] != 2 and le.args[0] != 3) or (le.args[0] != errno.ENOENT):
1920             excs.append(le)
1921hunk ./src/allmydata/util/fileutil.py 243
1922+    except Exception, le:
1923+        excs.append(le)
1924 
1925     # Okay, now we've recursively removed everything, ignoring any "No
1926     # such file or directory" errors, and collecting any other errors.
1927hunk ./src/allmydata/util/fileutil.py 256
1928             raise OSError, "Failed to remove dir for unknown reason."
1929         raise OSError, excs
1930 
1931+def fp_remove(dirfp):
1932+    """
1933+    An idempotent version of shutil.rmtree().  If the dir is already gone,
1934+    do nothing and return without raising an exception.  If this call
1935+    removes the dir, return without raising an exception.  If there is an
1936+    error that prevents removal or if the directory gets created again by
1937+    someone else after this deletes it and before this checks that it is
1938+    gone, raise an exception.
1939+    """
1940+    try:
1941+        dirfp.remove()
1942+    except UnlistableError, e:
1943+        if e.originalException.errno != errno.ENOENT:
1944+            raise
1945+    except OSError, e:
1946+        if e.errno != errno.ENOENT:
1947+            raise
1948+
1949+def rm_dir(dirname):
1950+    # Renamed to be like shutil.rmtree and unlike rmdir.
1951+    return rmtree(dirname)
1952 
1953 def remove_if_possible(f):
1954     try:
1955hunk ./src/allmydata/util/fileutil.py 387
1956         import traceback
1957         traceback.print_exc()
1958 
1959-def get_disk_stats(whichdir, reserved_space=0):
1960+def get_disk_stats(whichdirfp, reserved_space=0):
1961     """Return disk statistics for the storage disk, in the form of a dict
1962     with the following fields.
1963       total:            total bytes on disk
1964hunk ./src/allmydata/util/fileutil.py 408
1965     you can pass how many bytes you would like to leave unused on this
1966     filesystem as reserved_space.
1967     """
1968+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
1969 
1970     if have_GetDiskFreeSpaceExW:
1971         # If this is a Windows system and GetDiskFreeSpaceExW is available, use it.
1972hunk ./src/allmydata/util/fileutil.py 419
1973         n_free_for_nonroot = c_ulonglong(0)
1974         n_total            = c_ulonglong(0)
1975         n_free_for_root    = c_ulonglong(0)
1976-        retval = GetDiskFreeSpaceExW(whichdir, byref(n_free_for_nonroot),
1977+        retval = GetDiskFreeSpaceExW(whichdirfp.path, byref(n_free_for_nonroot),
1978                                                byref(n_total),
1979                                                byref(n_free_for_root))
1980         if retval == 0:
1981hunk ./src/allmydata/util/fileutil.py 424
1982             raise OSError("Windows error %d attempting to get disk statistics for %r"
1983-                          % (GetLastError(), whichdir))
1984+                          % (GetLastError(), whichdirfp.path))
1985         free_for_nonroot = n_free_for_nonroot.value
1986         total            = n_total.value
1987         free_for_root    = n_free_for_root.value
1988hunk ./src/allmydata/util/fileutil.py 433
1989         # <http://docs.python.org/library/os.html#os.statvfs>
1990         # <http://opengroup.org/onlinepubs/7990989799/xsh/fstatvfs.html>
1991         # <http://opengroup.org/onlinepubs/7990989799/xsh/sysstatvfs.h.html>
1992-        s = os.statvfs(whichdir)
1993+        s = os.statvfs(whichdirfp.path)
1994 
1995         # on my mac laptop:
1996         #  statvfs(2) is a wrapper around statfs(2).
1997hunk ./src/allmydata/util/fileutil.py 460
1998              'avail': avail,
1999            }
2000 
2001-def get_available_space(whichdir, reserved_space):
2002+def get_available_space(whichdirfp, reserved_space):
2003     """Returns available space for share storage in bytes, or None if no
2004     API to get this information is available.
2005 
2006hunk ./src/allmydata/util/fileutil.py 472
2007     you can pass how many bytes you would like to leave unused on this
2008     filesystem as reserved_space.
2009     """
2010+    precondition(isinstance(whichdirfp, FilePath), whichdirfp)
2011     try:
2012hunk ./src/allmydata/util/fileutil.py 474
2013-        return get_disk_stats(whichdir, reserved_space)['avail']
2014+        return get_disk_stats(whichdirfp, reserved_space)['avail']
2015     except AttributeError:
2016         return None
2017hunk ./src/allmydata/util/fileutil.py 477
2018-    except EnvironmentError:
2019-        log.msg("OS call to get disk statistics failed")
2020-        return 0
2021}
2022[add expirer.py
2023wilcoxjg@gmail.com**20110809203519
2024 Ignore-this: b09d460593f0e0aa065e867d5159455b
2025] {
2026addfile ./src/allmydata/storage/backends/das/expirer.py
2027hunk ./src/allmydata/storage/backends/das/expirer.py 1
2028+import time, os, pickle, struct # os, pickle, and struct will almost certainly be migrated to the backend...
2029+from allmydata.storage.crawler import ShareCrawler
2030+from allmydata.storage.common import UnknownMutableContainerVersionError, \
2031+     UnknownImmutableContainerVersionError
2032+from twisted.python import log as twlog
2033+
2034+class LeaseCheckingCrawler(ShareCrawler):
2035+    """I examine the leases on all shares, determining which are still valid
2036+    and which have expired. I can remove the expired leases (if so
2037+    configured), and the share will be deleted when the last lease is
2038+    removed.
2039+
2040+    I collect statistics on the leases and make these available to a web
2041+    status page, including:
2042+
2043+    Space recovered during this cycle-so-far:
2044+     actual (only if expiration_enabled=True):
2045+      num-buckets, num-shares, sum of share sizes, real disk usage
2046+      ('real disk usage' means we use stat(fn).st_blocks*512 and include any
2047+       space used by the directory)
2048+     what it would have been with the original lease expiration time
2049+     what it would have been with our configured expiration time
2050+
2051+    Prediction of space that will be recovered during the rest of this cycle
2052+    Prediction of space that will be recovered by the entire current cycle.
2053+
2054+    Space recovered during the last 10 cycles  <-- saved in separate pickle
2055+
2056+    Shares/buckets examined:
2057+     this cycle-so-far
2058+     prediction of rest of cycle
2059+     during last 10 cycles <-- separate pickle
2060+    start/finish time of last 10 cycles  <-- separate pickle
2061+    expiration time used for last 10 cycles <-- separate pickle
2062+
2063+    Histogram of leases-per-share:
2064+     this-cycle-to-date
2065+     last 10 cycles <-- separate pickle
2066+    Histogram of lease ages, buckets = 1day
2067+     cycle-to-date
2068+     last 10 cycles <-- separate pickle
2069+
2070+    All cycle-to-date values remain valid until the start of the next cycle.
2071+
2072+    """
2073+
2074+    slow_start = 360 # wait 6 minutes after startup
2075+    minimum_cycle_time = 12*60*60 # not more than twice per day
2076+
2077+    def __init__(self, statefile, historyfp, expiration_policy):
2078+        self.historyfp = historyfp
2079+        self.expiration_enabled = expiration_policy['enabled']
2080+        self.mode = expiration_policy['mode']
2081+        self.override_lease_duration = None
2082+        self.cutoff_date = None
2083+        if self.mode == "age":
2084+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
2085+            self.override_lease_duration = expiration_policy['override_lease_duration']# seconds
2086+        elif self.mode == "cutoff-date":
2087+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
2088+            assert cutoff_date is not None
2089+            self.cutoff_date = expiration_policy['cutoff_date']
2090+        else:
2091+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
2092+        self.sharetypes_to_expire = expiration_policy['sharetypes']
2093+        ShareCrawler.__init__(self, statefile)
2094+
2095+    def add_initial_state(self):
2096+        # we fill ["cycle-to-date"] here (even though they will be reset in
2097+        # self.started_cycle) just in case someone grabs our state before we
2098+        # get started: unit tests do this
2099+        so_far = self.create_empty_cycle_dict()
2100+        self.state.setdefault("cycle-to-date", so_far)
2101+        # in case we upgrade the code while a cycle is in progress, update
2102+        # the keys individually
2103+        for k in so_far:
2104+            self.state["cycle-to-date"].setdefault(k, so_far[k])
2105+
2106+        # initialize history
2107+        if not self.historyfp.exists():
2108+            history = {} # cyclenum -> dict
2109+            self.historyfp.setContent(pickle.dumps(history))
2110+
2111+    def create_empty_cycle_dict(self):
2112+        recovered = self.create_empty_recovered_dict()
2113+        so_far = {"corrupt-shares": [],
2114+                  "space-recovered": recovered,
2115+                  "lease-age-histogram": {}, # (minage,maxage)->count
2116+                  "leases-per-share-histogram": {}, # leasecount->numshares
2117+                  }
2118+        return so_far
2119+
2120+    def create_empty_recovered_dict(self):
2121+        recovered = {}
2122+        for a in ("actual", "original", "configured", "examined"):
2123+            for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2124+                recovered[a+"-"+b] = 0
2125+                recovered[a+"-"+b+"-mutable"] = 0
2126+                recovered[a+"-"+b+"-immutable"] = 0
2127+        return recovered
2128+
2129+    def started_cycle(self, cycle):
2130+        self.state["cycle-to-date"] = self.create_empty_cycle_dict()
2131+
2132+    def stat(self, fn):
2133+        return os.stat(fn)
2134+
2135+    def process_bucket(self, cycle, prefix, prefixdir, storage_index_b32):
2136+        bucketdir = os.path.join(prefixdir, storage_index_b32)
2137+        s = self.stat(bucketdir)
2138+        would_keep_shares = []
2139+        wks = None
2140+
2141+        for fn in os.listdir(bucketdir):
2142+            try:
2143+                shnum = int(fn)
2144+            except ValueError:
2145+                continue # non-numeric means not a sharefile
2146+            sharefile = os.path.join(bucketdir, fn)
2147+            try:
2148+                wks = self.process_share(sharefile)
2149+            except (UnknownMutableContainerVersionError,
2150+                    UnknownImmutableContainerVersionError,
2151+                    struct.error):
2152+                twlog.msg("lease-checker error processing %s" % sharefile)
2153+                twlog.err()
2154+                which = (storage_index_b32, shnum)
2155+                self.state["cycle-to-date"]["corrupt-shares"].append(which)
2156+                wks = (1, 1, 1, "unknown")
2157+            would_keep_shares.append(wks)
2158+
2159+        sharetype = None
2160+        if wks:
2161+            # use the last share's sharetype as the buckettype
2162+            sharetype = wks[3]
2163+        rec = self.state["cycle-to-date"]["space-recovered"]
2164+        self.increment(rec, "examined-buckets", 1)
2165+        if sharetype:
2166+            self.increment(rec, "examined-buckets-"+sharetype, 1)
2167+
2168+        try:
2169+            bucket_diskbytes = s.st_blocks * 512
2170+        except AttributeError:
2171+            bucket_diskbytes = 0 # no stat().st_blocks on windows
2172+        if sum([wks[0] for wks in would_keep_shares]) == 0:
2173+            self.increment_bucketspace("original", bucket_diskbytes, sharetype)
2174+        if sum([wks[1] for wks in would_keep_shares]) == 0:
2175+            self.increment_bucketspace("configured", bucket_diskbytes, sharetype)
2176+        if sum([wks[2] for wks in would_keep_shares]) == 0:
2177+            self.increment_bucketspace("actual", bucket_diskbytes, sharetype)
2178+
2179+    def process_share(self, sharefilename):
2180+        # first, find out what kind of a share it is
2181+        f = open(sharefilename, "rb")
2182+        prefix = f.read(32)
2183+        f.close()
2184+        if prefix == MutableShareFile.MAGIC:
2185+            sf = MutableShareFile(sharefilename)
2186+        else:
2187+            # otherwise assume it's immutable
2188+            sf = FSBShare(sharefilename)
2189+        sharetype = sf.sharetype
2190+        now = time.time()
2191+        s = self.stat(sharefilename)
2192+
2193+        num_leases = 0
2194+        num_valid_leases_original = 0
2195+        num_valid_leases_configured = 0
2196+        expired_leases_configured = []
2197+
2198+        for li in sf.get_leases():
2199+            num_leases += 1
2200+            original_expiration_time = li.get_expiration_time()
2201+            grant_renew_time = li.get_grant_renew_time_time()
2202+            age = li.get_age()
2203+            self.add_lease_age_to_histogram(age)
2204+
2205+            #  expired-or-not according to original expiration time
2206+            if original_expiration_time > now:
2207+                num_valid_leases_original += 1
2208+
2209+            #  expired-or-not according to our configured age limit
2210+            expired = False
2211+            if self.mode == "age":
2212+                age_limit = original_expiration_time
2213+                if self.override_lease_duration is not None:
2214+                    age_limit = self.override_lease_duration
2215+                if age > age_limit:
2216+                    expired = True
2217+            else:
2218+                assert self.mode == "cutoff-date"
2219+                if grant_renew_time < self.cutoff_date:
2220+                    expired = True
2221+            if sharetype not in self.sharetypes_to_expire:
2222+                expired = False
2223+
2224+            if expired:
2225+                expired_leases_configured.append(li)
2226+            else:
2227+                num_valid_leases_configured += 1
2228+
2229+        so_far = self.state["cycle-to-date"]
2230+        self.increment(so_far["leases-per-share-histogram"], num_leases, 1)
2231+        self.increment_space("examined", s, sharetype)
2232+
2233+        would_keep_share = [1, 1, 1, sharetype]
2234+
2235+        if self.expiration_enabled:
2236+            for li in expired_leases_configured:
2237+                sf.cancel_lease(li.cancel_secret)
2238+
2239+        if num_valid_leases_original == 0:
2240+            would_keep_share[0] = 0
2241+            self.increment_space("original", s, sharetype)
2242+
2243+        if num_valid_leases_configured == 0:
2244+            would_keep_share[1] = 0
2245+            self.increment_space("configured", s, sharetype)
2246+            if self.expiration_enabled:
2247+                would_keep_share[2] = 0
2248+                self.increment_space("actual", s, sharetype)
2249+
2250+        return would_keep_share
2251+
2252+    def increment_space(self, a, s, sharetype):
2253+        sharebytes = s.st_size
2254+        try:
2255+            # note that stat(2) says that st_blocks is 512 bytes, and that
2256+            # st_blksize is "optimal file sys I/O ops blocksize", which is
2257+            # independent of the block-size that st_blocks uses.
2258+            diskbytes = s.st_blocks * 512
2259+        except AttributeError:
2260+            # the docs say that st_blocks is only on linux. I also see it on
2261+            # MacOS. But it isn't available on windows.
2262+            diskbytes = sharebytes
2263+        so_far_sr = self.state["cycle-to-date"]["space-recovered"]
2264+        self.increment(so_far_sr, a+"-shares", 1)
2265+        self.increment(so_far_sr, a+"-sharebytes", sharebytes)
2266+        self.increment(so_far_sr, a+"-diskbytes", diskbytes)
2267+        if sharetype:
2268+            self.increment(so_far_sr, a+"-shares-"+sharetype, 1)
2269+            self.increment(so_far_sr, a+"-sharebytes-"+sharetype, sharebytes)
2270+            self.increment(so_far_sr, a+"-diskbytes-"+sharetype, diskbytes)
2271+
2272+    def increment_bucketspace(self, a, bucket_diskbytes, sharetype):
2273+        rec = self.state["cycle-to-date"]["space-recovered"]
2274+        self.increment(rec, a+"-diskbytes", bucket_diskbytes)
2275+        self.increment(rec, a+"-buckets", 1)
2276+        if sharetype:
2277+            self.increment(rec, a+"-diskbytes-"+sharetype, bucket_diskbytes)
2278+            self.increment(rec, a+"-buckets-"+sharetype, 1)
2279+
2280+    def increment(self, d, k, delta=1):
2281+        if k not in d:
2282+            d[k] = 0
2283+        d[k] += delta
2284+
2285+    def add_lease_age_to_histogram(self, age):
2286+        bucket_interval = 24*60*60
2287+        bucket_number = int(age/bucket_interval)
2288+        bucket_start = bucket_number * bucket_interval
2289+        bucket_end = bucket_start + bucket_interval
2290+        k = (bucket_start, bucket_end)
2291+        self.increment(self.state["cycle-to-date"]["lease-age-histogram"], k, 1)
2292+
2293+    def convert_lease_age_histogram(self, lah):
2294+        # convert { (minage,maxage) : count } into [ (minage,maxage,count) ]
2295+        # since the former is not JSON-safe (JSON dictionaries must have
2296+        # string keys).
2297+        json_safe_lah = []
2298+        for k in sorted(lah):
2299+            (minage,maxage) = k
2300+            json_safe_lah.append( (minage, maxage, lah[k]) )
2301+        return json_safe_lah
2302+
2303+    def finished_cycle(self, cycle):
2304+        # add to our history state, prune old history
2305+        h = {}
2306+
2307+        start = self.state["current-cycle-start-time"]
2308+        now = time.time()
2309+        h["cycle-start-finish-times"] = (start, now)
2310+        h["expiration-enabled"] = self.expiration_enabled
2311+        h["configured-expiration-mode"] = (self.mode,
2312+                                           self.override_lease_duration,
2313+                                           self.cutoff_date,
2314+                                           self.sharetypes_to_expire)
2315+
2316+        s = self.state["cycle-to-date"]
2317+
2318+        # state["lease-age-histogram"] is a dictionary (mapping
2319+        # (minage,maxage) tuple to a sharecount), but we report
2320+        # self.get_state()["lease-age-histogram"] as a list of
2321+        # (min,max,sharecount) tuples, because JSON can handle that better.
2322+        # We record the list-of-tuples form into the history for the same
2323+        # reason.
2324+        lah = self.convert_lease_age_histogram(s["lease-age-histogram"])
2325+        h["lease-age-histogram"] = lah
2326+        h["leases-per-share-histogram"] = s["leases-per-share-histogram"].copy()
2327+        h["corrupt-shares"] = s["corrupt-shares"][:]
2328+        # note: if ["shares-recovered"] ever acquires an internal dict, this
2329+        # copy() needs to become a deepcopy
2330+        h["space-recovered"] = s["space-recovered"].copy()
2331+
2332+        history = pickle.load(self.historyfp.getContent())
2333+        history[cycle] = h
2334+        while len(history) > 10:
2335+            oldcycles = sorted(history.keys())
2336+            del history[oldcycles[0]]
2337+        self.historyfp.setContent(pickle.dumps(history))
2338+
2339+    def get_state(self):
2340+        """In addition to the crawler state described in
2341+        ShareCrawler.get_state(), I return the following keys which are
2342+        specific to the lease-checker/expirer. Note that the non-history keys
2343+        (with 'cycle' in their names) are only present if a cycle is
2344+        currently running. If the crawler is between cycles, it appropriate
2345+        to show the latest item in the 'history' key instead. Also note that
2346+        each history item has all the data in the 'cycle-to-date' value, plus
2347+        cycle-start-finish-times.
2348+
2349+         cycle-to-date:
2350+          expiration-enabled
2351+          configured-expiration-mode
2352+          lease-age-histogram (list of (minage,maxage,sharecount) tuples)
2353+          leases-per-share-histogram
2354+          corrupt-shares (list of (si_b32,shnum) tuples, minimal verification)
2355+          space-recovered
2356+
2357+         estimated-remaining-cycle:
2358+          # Values may be None if not enough data has been gathered to
2359+          # produce an estimate.
2360+          space-recovered
2361+
2362+         estimated-current-cycle:
2363+          # cycle-to-date plus estimated-remaining. Values may be None if
2364+          # not enough data has been gathered to produce an estimate.
2365+          space-recovered
2366+
2367+         history: maps cyclenum to a dict with the following keys:
2368+          cycle-start-finish-times
2369+          expiration-enabled
2370+          configured-expiration-mode
2371+          lease-age-histogram
2372+          leases-per-share-histogram
2373+          corrupt-shares
2374+          space-recovered
2375+
2376+         The 'space-recovered' structure is a dictionary with the following
2377+         keys:
2378+          # 'examined' is what was looked at
2379+          examined-buckets, examined-buckets-mutable, examined-buckets-immutable
2380+          examined-shares, -mutable, -immutable
2381+          examined-sharebytes, -mutable, -immutable
2382+          examined-diskbytes, -mutable, -immutable
2383+
2384+          # 'actual' is what was actually deleted
2385+          actual-buckets, -mutable, -immutable
2386+          actual-shares, -mutable, -immutable
2387+          actual-sharebytes, -mutable, -immutable
2388+          actual-diskbytes, -mutable, -immutable
2389+
2390+          # would have been deleted, if the original lease timer was used
2391+          original-buckets, -mutable, -immutable
2392+          original-shares, -mutable, -immutable
2393+          original-sharebytes, -mutable, -immutable
2394+          original-diskbytes, -mutable, -immutable
2395+
2396+          # would have been deleted, if our configured max_age was used
2397+          configured-buckets, -mutable, -immutable
2398+          configured-shares, -mutable, -immutable
2399+          configured-sharebytes, -mutable, -immutable
2400+          configured-diskbytes, -mutable, -immutable
2401+
2402+        """
2403+        progress = self.get_progress()
2404+
2405+        state = ShareCrawler.get_state(self) # does a shallow copy
2406+        history = pickle.load(self.historyfp.getContent())
2407+        state["history"] = history
2408+
2409+        if not progress["cycle-in-progress"]:
2410+            del state["cycle-to-date"]
2411+            return state
2412+
2413+        so_far = state["cycle-to-date"].copy()
2414+        state["cycle-to-date"] = so_far
2415+
2416+        lah = so_far["lease-age-histogram"]
2417+        so_far["lease-age-histogram"] = self.convert_lease_age_histogram(lah)
2418+        so_far["expiration-enabled"] = self.expiration_enabled
2419+        so_far["configured-expiration-mode"] = (self.mode,
2420+                                                self.override_lease_duration,
2421+                                                self.cutoff_date,
2422+                                                self.sharetypes_to_expire)
2423+
2424+        so_far_sr = so_far["space-recovered"]
2425+        remaining_sr = {}
2426+        remaining = {"space-recovered": remaining_sr}
2427+        cycle_sr = {}
2428+        cycle = {"space-recovered": cycle_sr}
2429+
2430+        if progress["cycle-complete-percentage"] > 0.0:
2431+            pc = progress["cycle-complete-percentage"] / 100.0
2432+            m = (1-pc)/pc
2433+            for a in ("actual", "original", "configured", "examined"):
2434+                for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2435+                    for c in ("", "-mutable", "-immutable"):
2436+                        k = a+"-"+b+c
2437+                        remaining_sr[k] = m * so_far_sr[k]
2438+                        cycle_sr[k] = so_far_sr[k] + remaining_sr[k]
2439+        else:
2440+            for a in ("actual", "original", "configured", "examined"):
2441+                for b in ("buckets", "shares", "sharebytes", "diskbytes"):
2442+                    for c in ("", "-mutable", "-immutable"):
2443+                        k = a+"-"+b+c
2444+                        remaining_sr[k] = None
2445+                        cycle_sr[k] = None
2446+
2447+        state["estimated-remaining-cycle"] = remaining
2448+        state["estimated-current-cycle"] = cycle
2449+        return state
2450}
2451[Changes I have made that aren't necessary for the test_backends.py suite to pass.
2452wilcoxjg@gmail.com**20110809203811
2453 Ignore-this: 117d49047456013f382ffc0559f00c40
2454] {
2455hunk ./src/allmydata/storage/crawler.py 1
2456-
2457 import os, time, struct
2458 import cPickle as pickle
2459 from twisted.internet import reactor
2460hunk ./src/allmydata/storage/crawler.py 6
2461 from twisted.application import service
2462 from allmydata.storage.common import si_b2a
2463-from allmydata.util import fileutil
2464 
2465 class TimeSliceExceeded(Exception):
2466     pass
2467hunk ./src/allmydata/storage/crawler.py 11
2468 
2469 class ShareCrawler(service.MultiService):
2470-    """A ShareCrawler subclass is attached to a StorageServer, and
2471+    """A subclass of ShareCrawler is attached to a StorageServer, and
2472     periodically walks all of its shares, processing each one in some
2473     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
2474     since large servers can easily have a terabyte of shares, in several
2475hunk ./src/allmydata/storage/crawler.py 29
2476     We assume that the normal upload/download/get_buckets traffic of a tahoe
2477     grid will cause the prefixdir contents to be mostly cached in the kernel,
2478     or that the number of buckets in each prefixdir will be small enough to
2479-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
2480+    load quickly. A 1TB allmydata.com server was measured to have 2.56 * 10^6
2481     buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
2482     prefix. On this server, each prefixdir took 130ms-200ms to list the first
2483     time, and 17ms to list the second time.
2484hunk ./src/allmydata/storage/crawler.py 66
2485     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
2486     minimum_cycle_time = 300 # don't run a cycle faster than this
2487 
2488-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
2489+    def __init__(self, statefp, allowed_cpu_percentage=None):
2490         service.MultiService.__init__(self)
2491         if allowed_cpu_percentage is not None:
2492             self.allowed_cpu_percentage = allowed_cpu_percentage
2493hunk ./src/allmydata/storage/crawler.py 70
2494-        self.server = server
2495-        self.sharedir = server.sharedir
2496-        self.statefile = statefile
2497+        self.statefp = statefp
2498         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
2499                          for i in range(2**10)]
2500         self.prefixes.sort()
2501hunk ./src/allmydata/storage/crawler.py 190
2502         #                            of the last bucket to be processed, or
2503         #                            None if we are sleeping between cycles
2504         try:
2505-            f = open(self.statefile, "rb")
2506-            state = pickle.load(f)
2507-            f.close()
2508+            state = pickle.loads(self.statefp.getContent())
2509         except EnvironmentError:
2510             state = {"version": 1,
2511                      "last-cycle-finished": None,
2512hunk ./src/allmydata/storage/crawler.py 226
2513         else:
2514             last_complete_prefix = self.prefixes[lcpi]
2515         self.state["last-complete-prefix"] = last_complete_prefix
2516-        tmpfile = self.statefile + ".tmp"
2517-        f = open(tmpfile, "wb")
2518-        pickle.dump(self.state, f)
2519-        f.close()
2520-        fileutil.move_into_place(tmpfile, self.statefile)
2521+        self.statefp.setContent(pickle.dumps(self.state))
2522 
2523     def startService(self):
2524         # arrange things to look like we were just sleeping, so
2525hunk ./src/allmydata/storage/crawler.py 438
2526 
2527     minimum_cycle_time = 60*60 # we don't need this more than once an hour
2528 
2529-    def __init__(self, server, statefile, num_sample_prefixes=1):
2530-        ShareCrawler.__init__(self, server, statefile)
2531+    def __init__(self, statefp, num_sample_prefixes=1):
2532+        ShareCrawler.__init__(self, statefp)
2533         self.num_sample_prefixes = num_sample_prefixes
2534 
2535     def add_initial_state(self):
2536hunk ./src/allmydata/storage/crawler.py 478
2537             old_cycle,buckets = self.state["storage-index-samples"][prefix]
2538             if old_cycle != cycle:
2539                 del self.state["storage-index-samples"][prefix]
2540-
2541hunk ./src/allmydata/storage/lease.py 17
2542 
2543     def get_expiration_time(self):
2544         return self.expiration_time
2545+
2546     def get_grant_renew_time_time(self):
2547         # hack, based upon fixed 31day expiration period
2548         return self.expiration_time - 31*24*60*60
2549hunk ./src/allmydata/storage/lease.py 21
2550+
2551     def get_age(self):
2552         return time.time() - self.get_grant_renew_time_time()
2553 
2554hunk ./src/allmydata/storage/lease.py 32
2555          self.expiration_time) = struct.unpack(">L32s32sL", data)
2556         self.nodeid = None
2557         return self
2558+
2559     def to_immutable_data(self):
2560         return struct.pack(">L32s32sL",
2561                            self.owner_num,
2562hunk ./src/allmydata/storage/lease.py 45
2563                            int(self.expiration_time),
2564                            self.renew_secret, self.cancel_secret,
2565                            self.nodeid)
2566+
2567     def from_mutable_data(self, data):
2568         (self.owner_num,
2569          self.expiration_time,
2570}
2571[add __init__.py to backend and core and null
2572wilcoxjg@gmail.com**20110810033751
2573 Ignore-this: 1c72bc54951033ab433c38de58bdc39c
2574] {
2575addfile ./src/allmydata/storage/backends/__init__.py
2576addfile ./src/allmydata/storage/backends/null/__init__.py
2577}
2578[whitespace-cleanup
2579wilcoxjg@gmail.com**20110810170847
2580 Ignore-this: 7a278e7c87c6fcd2e5ed783667c8b746
2581] {
2582hunk ./src/allmydata/interfaces.py 1
2583-
2584 from zope.interface import Interface
2585 from foolscap.api import StringConstraint, ListOf, TupleOf, SetOf, DictOf, \
2586      ChoiceOf, IntegerConstraint, Any, RemoteInterface, Referenceable
2587hunk ./src/allmydata/storage/backends/das/core.py 47
2588         self._setup_lease_checkerf(expiration_policy)
2589 
2590     def _setup_storage(self, storedir, readonly, reserved_space):
2591-        precondition(isinstance(storedir, FilePath), storedir, FilePath) 
2592+        precondition(isinstance(storedir, FilePath), storedir, FilePath)
2593         self.storedir = storedir
2594         self.readonly = readonly
2595         self.reserved_space = int(reserved_space)
2596hunk ./src/allmydata/storage/backends/das/core.py 89
2597         except UnlistableError:
2598             # There is no shares directory at all.
2599             return frozenset()
2600-           
2601+
2602     def get_shares(self, storageindex):
2603         """ Generate ImmutableShare objects for shares we have for this
2604         storageindex. ("Shares we have" means completed ones, excluding
2605hunk ./src/allmydata/storage/backends/das/core.py 104
2606         except UnlistableError:
2607             # There is no shares directory at all.
2608             pass
2609-       
2610+
2611     def get_available_space(self):
2612         if self.readonly:
2613             return 0
2614hunk ./src/allmydata/storage/backends/das/core.py 122
2615 
2616     def set_storage_server(self, ss):
2617         self.ss = ss
2618-       
2619+
2620 
2621 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
2622 # and share data. The share data is accessed by RIBucketWriter.write and
2623hunk ./src/allmydata/storage/backends/das/core.py 223
2624             # above-mentioned conditions.
2625             pass
2626         pass
2627-       
2628+
2629     def stat(self):
2630         return filepath.stat(self.finalhome.path)[stat.ST_SIZE]
2631 
2632hunk ./src/allmydata/storage/backends/das/core.py 309
2633         num_leases = self._read_num_leases(self.incominghome)
2634         self._write_lease_record(self.incominghome, num_leases, lease_info)
2635         self._write_num_leases(self.incominghome, num_leases+1)
2636-       
2637+
2638     def renew_lease(self, renew_secret, new_expire_time):
2639         for i,lease in enumerate(self.get_leases()):
2640             if constant_time_compare(lease.renew_secret, renew_secret):
2641hunk ./src/allmydata/storage/common.py 1
2642-
2643 import os.path
2644 from allmydata.util import base32
2645 
2646hunk ./src/allmydata/storage/server.py 149
2647 
2648         if self.readonly_storage:
2649             return 0
2650-        return fileutil.get_available_space(self.storedir, self.reserved_space)
2651+        return fileutil.get_available_space(self.sharedir, self.reserved_space)
2652 
2653     def allocated_size(self):
2654         space = 0
2655hunk ./src/allmydata/storage/server.py 346
2656         self.count("writev")
2657         si_s = si_b2a(storageindex)
2658         log.msg("storage: slot_writev %s" % si_s)
2659-       
2660+
2661         (write_enabler, renew_secret, cancel_secret) = secrets
2662         # shares exist if there is a file for them
2663         bucketdir = si_si2dir(self.sharedir, storageindex)
2664}
2665[das/__init__.py
2666wilcoxjg@gmail.com**20110810173849
2667 Ignore-this: bdb730cba1d53d8827ef5fef65958471
2668] addfile ./src/allmydata/storage/backends/das/__init__.py
2669[test_backends.py: cleaned whitespace and removed unused variables
2670wilcoxjg@gmail.com**20110810201041
2671 Ignore-this: d000d4a7d3a0793464306e9d09437be6
2672] {
2673hunk ./src/allmydata/test/test_backends.py 13
2674 from allmydata.storage.common import si_si2dir
2675 # The following share file content was generated with
2676 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
2677-# with share data == 'a'. The total size of this input
2678+# with share data == 'a'. The total size of this input
2679 # is 85 bytes.
2680 shareversionnumber = '\x00\x00\x00\x01'
2681 sharedatalength = '\x00\x00\x00\x01'
2682hunk ./src/allmydata/test/test_backends.py 29
2683     cancelsecret + expirationtime + nextlease
2684 share_data = containerdata + client_data
2685 testnodeid = 'testnodeidxxxxxxxxxx'
2686-expiration_policy = {'enabled' : False,
2687+expiration_policy = {'enabled' : False,
2688                      'mode' : 'age',
2689                      'override_lease_duration' : None,
2690                      'cutoff_date' : None,
2691hunk ./src/allmydata/test/test_backends.py 37
2692 
2693 
2694 class MockFileSystem(unittest.TestCase):
2695-    """ I simulate a filesystem that the code under test can use. I simulate
2696-    just the parts of the filesystem that the current implementation of DAS
2697+    """ I simulate a filesystem that the code under test can use. I simulate
2698+    just the parts of the filesystem that the current implementation of DAS
2699     backend needs. """
2700     def setUp(self):
2701         # Make patcher, patch, and make effects for fs using functions.
2702hunk ./src/allmydata/test/test_backends.py 43
2703         msg( "%s.setUp()" % (self,))
2704-        self.mockedfilepaths = {}
2705+        self.mockedfilepaths = {}
2706         #keys are pathnames, values are MockFilePath objects. This is necessary because
2707         #MockFilePath behavior sometimes depends on the filesystem. Where it does,
2708         #self.mockedfilepaths has the relevent info.
2709hunk ./src/allmydata/test/test_backends.py 56
2710         self.sharefinalname = self.sharedirfinalname.child('0')
2711 
2712         self.FilePathFake = mock.patch('allmydata.storage.backends.das.core.FilePath', new = MockFilePath )
2713-        FakePath = self.FilePathFake.__enter__()
2714+        self.FilePathFake.__enter__()
2715 
2716         self.BCountingCrawler = mock.patch('allmydata.storage.backends.das.core.BucketCountingCrawler')
2717         FakeBCC = self.BCountingCrawler.__enter__()
2718hunk ./src/allmydata/test/test_backends.py 89
2719 
2720     def tearDown(self):
2721         msg( "%s.tearDown()" % (self,))
2722-        FakePath = self.FilePathFake.__exit__()       
2723+        self.FilePathFake.__exit__()
2724         self.mockedfilepaths = {}
2725 
2726 
2727hunk ./src/allmydata/test/test_backends.py 116
2728         self.mockedfilepaths[self.path].fileobject = self.fileobject
2729         self.mockedfilepaths[self.path].existance = self.existance
2730         self.setparents()
2731-       
2732+
2733     def create(self):
2734         # This method chokes if there's a pre-existing file!
2735         if self.mockedfilepaths[self.path].fileobject:
2736hunk ./src/allmydata/test/test_backends.py 122
2737             raise OSError
2738         else:
2739-            self.fileobject = MockFileObject(contentstring)
2740             self.existance = True
2741             self.mockedfilepaths[self.path].fileobject = self.fileobject
2742             self.mockedfilepaths[self.path].existance = self.existance
2743hunk ./src/allmydata/test/test_backends.py 125
2744-            self.setparents()       
2745+            self.setparents()
2746 
2747     def open(self, mode='r'):
2748         # XXX Makes no use of mode.
2749hunk ./src/allmydata/test/test_backends.py 151
2750         childrenfromffs = [ffp for ffp in childrenfromffs if not ffp.path.endswith(self.path)]
2751         childrenfromffs = [ffp for ffp in childrenfromffs if ffp.exists()]
2752         self.spawn = frozenset(childrenfromffs)
2753-        return self.spawn 
2754+        return self.spawn
2755 
2756     def parent(self):
2757         if self.mockedfilepaths.has_key(self.antecedent):
2758hunk ./src/allmydata/test/test_backends.py 163
2759     def parents(self):
2760         antecedents = []
2761         def f(fps, antecedents):
2762-            newfps = os.path.split(fps)[0]
2763+            newfps = os.path.split(fps)[0]
2764             if newfps:
2765                 antecedents.append(newfps)
2766                 f(newfps, antecedents)
2767hunk ./src/allmydata/test/test_backends.py 256
2768     @mock.patch('os.listdir')
2769     @mock.patch('os.path.isdir')
2770     def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
2771-        """ Write a new share. """
2772+        """ 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. """
2773 
2774         alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2775         bs[0].remote_write(0, 'a')
2776hunk ./src/allmydata/test/test_backends.py 275
2777 
2778 
2779 class TestServerAndFSBackend(MockFileSystem, ReallyEqualMixin):
2780-    """ This tests both the StorageServer and the DAS backend together. """   
2781+    """ This tests both the StorageServer and the DAS backend together. """
2782     def setUp(self):
2783         MockFileSystem.setUp(self)
2784         try:
2785hunk ./src/allmydata/test/test_backends.py 292
2786     @mock.patch('allmydata.util.fileutil.get_available_space')
2787     def test_out_of_space(self, mockget_available_space, mocktime):
2788         mocktime.return_value = 0
2789-       
2790+
2791         def call_get_available_space(dir, reserve):
2792             return 0
2793 
2794hunk ./src/allmydata/test/test_backends.py 310
2795         mocktime.return_value = 0
2796         # Inspect incoming and fail unless it's empty.
2797         incomingset = self.ss.backend.get_incoming_shnums('teststorage_index')
2798-       
2799+
2800         self.failUnlessReallyEqual(incomingset, frozenset())
2801hunk ./src/allmydata/test/test_backends.py 312
2802-       
2803+
2804         # Populate incoming with the sharenum: 0.
2805         alreadygot, bs = self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, frozenset((0,)), 1, mock.Mock())
2806 
2807hunk ./src/allmydata/test/test_backends.py 329
2808         # has been called.
2809         self.failIf(bsa)
2810 
2811-        # Test allocated size.
2812+        # Test allocated size.
2813         spaceint = self.ss.allocated_size()
2814         self.failUnlessReallyEqual(spaceint, 1)
2815 
2816hunk ./src/allmydata/test/test_backends.py 335
2817         # Write 'a' to shnum 0. Only tested together with close and read.
2818         bs[0].remote_write(0, 'a')
2819-       
2820+
2821         # Preclose: Inspect final, failUnless nothing there.
2822         self.failUnlessReallyEqual(len(list(self.backend.get_shares('teststorage_index'))), 0)
2823         bs[0].remote_close()
2824hunk ./src/allmydata/test/test_backends.py 349
2825         # Exercise the case that the share we're asking to allocate is
2826         # already (completely) uploaded.
2827         self.ss.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
2828-       
2829+
2830 
2831     def test_read_old_share(self):
2832         """ This tests whether the code correctly finds and reads
2833hunk ./src/allmydata/test/test_backends.py 360
2834         StorageServer object. """
2835         # Contruct a file with the appropriate contents in the mockfilesystem.
2836         datalen = len(share_data)
2837-        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2838+        finalhome = si_si2dir(self.basedir, 'teststorage_index').child(str(0))
2839         finalhome.setContent(share_data)
2840 
2841         # Now begin the test.
2842}
2843
2844Context:
2845
2846[test_client.py: relax a check in test_create_drop_uploader so that it should pass on Python 2.4.x. refs #1429
2847david-sarah@jacaranda.org**20110810052504
2848 Ignore-this: 1380749ceaf33c30e26c50d57476616c
2849]
2850[test/common_util.py: correct fix to mkdir_nonascii. refs #1472
2851david-sarah@jacaranda.org**20110810051906
2852 Ignore-this: 93c0c33370bc47d95c26c4cce8e05290
2853]
2854[test/common_util.py: fix a typo. refs #1472
2855david-sarah@jacaranda.org**20110810044235
2856 Ignore-this: f88643d7c82cb3577686d77bbff9e2bc
2857]
2858[test_client.py, test_drop_upload.py: fix pyflakes warnings.
2859david-sarah@jacaranda.org**20110810034505
2860 Ignore-this: 1e2d71bf2f43d63cbb423d32a6f96793
2861]
2862[Factor out methods dealing with non-ASCII directories and filenames from test_drop_upload.py into common_util.py. refs #1429, #1472
2863david-sarah@jacaranda.org**20110810031558
2864 Ignore-this: 3de8f945fa7a58fc318a1184bad0fd1a
2865]
2866[test_client.py: add a test that the drop-uploader is initialized correctly by client.py. Also give the DropUploader service a name, which is necessary for the test. refs #1429
2867david-sarah@jacaranda.org**20110810030538
2868 Ignore-this: 13d511ea9bbe9da2dcffe4a91ce94eae
2869]
2870[drop-upload: rename 'start' method to 'startService', which is what you're supposed to use to start a Service. refs #1429
2871david-sarah@jacaranda.org**20110810030345
2872 Ignore-this: d1f5e5c63937ea37be37324e2f1ae99d
2873]
2874[test_drop_upload.py: add comment explaining why we don't use FilePath.setContent. refs #1429
2875david-sarah@jacaranda.org**20110810025942
2876 Ignore-this: b95358030b63cb467d1d7f1b9a9b6978
2877]
2878[test_drop_upload.py: fix some grammatical and spelling nits. refs #1429
2879david-sarah@jacaranda.org**20110809221231
2880 Ignore-this: fd331acddd9f754173f274a34fe62f03
2881]
2882[drop-upload: report the configured local directory being absent differently from it being a file
2883zooko@zooko.com**20110809220930
2884 Ignore-this: a08879100f5f20e609be3f0ffa3b25cc
2885 refs #1429
2886]
2887[drop-upload: rename the 'upload.uri' parameter to 'upload.dircap', and a couple of cleanups to error messages. refs #1429
2888zooko@zooko.com**20110809220508
2889 Ignore-this: 4846368cbe331e8653bdce1f314e276b
2890 I rerecorded this patch, originally by David-Sarah, to use "darcs replace" instead of editing to do the renames. This uncovered one missed rename in Client.init_drop_uploader. (Which also means that code isn't exercised by the current unit tests.)
2891 refs #1429
2892]
2893[drop-upload test for non-existent local dir separately from test for non-directory local dir
2894zooko@zooko.com**20110809220115
2895 Ignore-this: cd85f345c02f5cb71b1c1527bd4ebddc
2896 A candidate patch for #1429 has a bug when it is using FilePath.is_dir() to detect whether the configured local dir exists and is a directory. FilePath.is_dir() raises exception, instead of returning False, if the thing doesn't exist. This test is to make sure that DropUploader.__init__ raise different exceptions for those two cases.
2897 refs #1429
2898]
2899[drop-upload: unit tests for the configuration options being named "cap" instead of "uri"
2900zooko@zooko.com**20110809215913
2901 Ignore-this: 958c78fffb3d76b3e4817647f824e7f9
2902 This is a subset of a patch that David-Sarah attached to #1429. This is just the unit-tests part of that patch, and uses darcs record instead of hunks to change the names.
2903 refs #1429
2904]
2905[src/allmydata/storage/server.py: use the filesystem of storage/shares/, rather than storage/, to calculate remaining space. fixes #1384
2906david-sarah@jacaranda.org**20110719022752
2907 Ignore-this: a4781043cfd453dbb66ae4f108d80bea
2908]
2909[test_storage.py: test that we are using the filesystem of storage/shares/, rather than storage/, to calculate remaining space, and that the HTML status output reflects the values returned by fileutil.get_disk_stats. This version works with older versions of the mock library. refs #1384
2910david-sarah@jacaranda.org**20110809190722
2911 Ignore-this: db447caca37a459ca49563efa58db58c
2912]
2913[Work around ref #1472 by having test_drop_upload delete the non-ASCII directories it creates.
2914david-sarah@jacaranda.org**20110809012334
2915 Ignore-this: 5881fd5db419ba8ad12e0b2a82f6c4f0
2916]
2917[Remove all trailing whitespace from .py files.
2918david-sarah@jacaranda.org**20110809001117
2919 Ignore-this: d2658b5ce44af70cc606ae4d3085b7cc
2920]
2921[test_drop_upload.py: fix unused imports. refs #1429
2922david-sarah@jacaranda.org**20110808235422
2923 Ignore-this: 834f6b946bfea699d7d8c743edd66671
2924]
2925[Documentation for drop-upload frontend. refs #1429
2926david-sarah@jacaranda.org**20110808182146
2927 Ignore-this: b33110834e586c0b784d1736c2af5779
2928]
2929[Drop-upload frontend, rerecorded for 1.9 beta (and correcting a minor mistake). Includes some fixes for Windows but not the Windows inotify implementation. fixes #1429
2930david-sarah@jacaranda.org**20110808234049
2931 Ignore-this: 67f824c7f554e9a3a85f9fd2e1123d97
2932]
2933[node.py: ensure that client and introducer nodes record their port number and use that port on the next restart, fixing a regression caused by #1385. fixes #1469.
2934david-sarah@jacaranda.org**20110806221934
2935 Ignore-this: 1aa9d340b6570320ab2f9edc89c9e0a8
2936]
2937[test_runner.py: fix a race condition in the test when NODE_URL_FILE is written before PORTNUM_FILE. refs #1469
2938david-sarah@jacaranda.org**20110806231842
2939 Ignore-this: ab01ae7cec3a073e29eec473e64052a0
2940]
2941[test_runner.py: cleanups of HOTLINE_FILE writing and removal.
2942david-sarah@jacaranda.org**20110806231652
2943 Ignore-this: 25f5c5d6f5d8faebb26a4ce80110a335
2944]
2945[test_runner.py: remove an unused constant.
2946david-sarah@jacaranda.org**20110806221416
2947 Ignore-this: eade2695cbabbea9cafeaa8debe410bb
2948]
2949[node.py: fix the error path for a missing config option so that it works for a Unicode base directory.
2950david-sarah@jacaranda.org**20110806221007
2951 Ignore-this: 4eb9cc04b2ce05182a274a0d69dafaf3
2952]
2953[test_runner.py: test that client and introducer nodes record their port number and use that port on the next restart. This tests for a regression caused by ref #1385.
2954david-sarah@jacaranda.org**20110806220635
2955 Ignore-this: 40a0c040b142dbddd47e69b3c3712f5
2956]
2957[test_runner.py: fix a bug in CreateNode.do_create introduced in changeset [5114] when the tahoe.cfg file has been written with CRLF line endings. refs #1385
2958david-sarah@jacaranda.org**20110804003032
2959 Ignore-this: 7b7afdcf99da6671afac2d42828883eb
2960]
2961[test_client.py: repair Basic.test_error_on_old_config_files. refs #1385
2962david-sarah@jacaranda.org**20110803235036
2963 Ignore-this: 31e2a9c3febe55948de7e144353663e
2964]
2965[test_checker.py: increase timeout for TooParallel.test_immutable again. The ARM buildslave took 38 seconds, so 40 seconds is too close to the edge; make it 80.
2966david-sarah@jacaranda.org**20110803214042
2967 Ignore-this: 2d8026a6b25534e01738f78d6c7495cb
2968]
2969[test_runner.py: fix RunNode.test_introducer to not rely on the mtime of introducer.furl to detect when the node has restarted. Instead we detect when node.url has been written. refs #1385
2970david-sarah@jacaranda.org**20110803180917
2971 Ignore-this: 11ddc43b107beca42cb78af88c5c394c
2972]
2973[Further improve error message about old config files. refs #1385
2974david-sarah@jacaranda.org**20110803174546
2975 Ignore-this: 9d6cc3c288d9863dce58faafb3855917
2976]
2977[Slightly improve error message about old config files (avoid unnecessary Unicode escaping). refs #1385
2978david-sarah@jacaranda.org**20110803163848
2979 Ignore-this: a3e3930fba7ccf90b8db3d2ed5829df4
2980]
2981[test_checker.py: increase timeout for TooParallel.test_immutable (was consistently failing on ARM buildslave).
2982david-sarah@jacaranda.org**20110803163213
2983 Ignore-this: d0efceaf12628e8791862b80c85b5d56
2984]
2985[Fix the bug that prevents an introducer from starting when introducer.furl already exists. Also remove some dead code that used to read old config files, and rename 'warn_about_old_config_files' to reflect that it's not a warning. refs #1385
2986david-sarah@jacaranda.org**20110803013212
2987 Ignore-this: 2d6cd14bd06a7493b26f2027aff78f4d
2988]
2989[test_runner.py: modify RunNode.test_introducer to test that starting an introducer works when the introducer.furl file already exists. refs #1385
2990david-sarah@jacaranda.org**20110803012704
2991 Ignore-this: 8cf7f27ac4bfbb5ad8ca4a974106d437
2992]
2993[verifier: correct a bug introduced in changeset [5106] that caused us to only verify the first block of a file. refs #1395
2994david-sarah@jacaranda.org**20110802172437
2995 Ignore-this: 87fb77854a839ff217dce73544775b11
2996]
2997[test_repairer: add a deterministic test of share data corruption that always flips the bits of the last byte of the share data. refs #1395
2998david-sarah@jacaranda.org**20110802175841
2999 Ignore-this: 72f54603785007e88220c8d979e08be7
3000]
3001[verifier: serialize the fetching of blocks within a share so that we don't use too much RAM
3002zooko@zooko.com**20110802063703
3003 Ignore-this: debd9bac07dcbb6803f835a9e2eabaa1
3004 
3005 Shares are still verified in parallel, but within a share, don't request a
3006 block until the previous block has been verified and the memory we used to hold
3007 it has been freed up.
3008 
3009 Patch originally due to Brian. This version has a mockery-patchery-style test
3010 which is "low tech" (it implements the patching inline in the test code instead
3011 of using an extension of the mock.patch() function from the mock library) and
3012 which unpatches in case of exception.
3013 
3014 fixes #1395
3015]
3016[add docs about timing-channel attacks
3017Brian Warner <warner@lothar.com>**20110802044541
3018 Ignore-this: 73114d5f5ed9ce252597b707dba3a194
3019]
3020['test-coverage' now needs PYTHONPATH=. to find TOP/twisted/plugins/
3021Brian Warner <warner@lothar.com>**20110802041952
3022 Ignore-this: d40f1f4cb426ea1c362fc961baedde2
3023]
3024[remove nodeid from WriteBucketProxy classes and customers
3025warner@lothar.com**20110801224317
3026 Ignore-this: e55334bb0095de11711eeb3af827e8e8
3027 refs #1363
3028]
3029[remove get_serverid() from ReadBucketProxy and customers, including Checker
3030warner@lothar.com**20110801224307
3031 Ignore-this: 837aba457bc853e4fd413ab1a94519cb
3032 and debug.py dump-share commands
3033 refs #1363
3034]
3035[reject old-style (pre-Tahoe-LAFS-v1.3) configuration files
3036zooko@zooko.com**20110801232423
3037 Ignore-this: b58218fcc064cc75ad8f05ed0c38902b
3038 Check for the existence of any of them and if any are found raise exception which will abort the startup of the node.
3039 This is a backwards-incompatible change for anyone who is still using old-style configuration files.
3040 fixes #1385
3041]
3042[whitespace-cleanup
3043zooko@zooko.com**20110725015546
3044 Ignore-this: 442970d0545183b97adc7bd66657876c
3045]
3046[tests: use fileutil.write() instead of open() to ensure timely close even without CPython-style reference counting
3047zooko@zooko.com**20110331145427
3048 Ignore-this: 75aae4ab8e5fa0ad698f998aaa1888ce
3049 Some of these already had an explicit close() but I went ahead and replaced them with fileutil.write() as well for the sake of uniformity.
3050]
3051[Address Kevan's comment in #776 about Options classes missed when adding 'self.command_name'. refs #776, #1359
3052david-sarah@jacaranda.org**20110801221317
3053 Ignore-this: 8881d42cf7e6a1d15468291b0cb8fab9
3054]
3055[docs/frontends/webapi.rst: change some more instances of 'delete' or 'remove' to 'unlink', change some section titles, and use two blank lines between all sections. refs #776, #1104
3056david-sarah@jacaranda.org**20110801220919
3057 Ignore-this: 572327591137bb05c24c44812d4b163f
3058]
3059[cleanup: implement rm as a synonym for unlink rather than vice-versa. refs #776
3060david-sarah@jacaranda.org**20110801220108
3061 Ignore-this: 598dcbed870f4f6bb9df62de9111b343
3062]
3063[docs/webapi.rst: address Kevan's comments about use of 'delete' on ref #1104
3064david-sarah@jacaranda.org**20110801205356
3065 Ignore-this: 4fbf03864934753c951ddeff64392491
3066]
3067[docs: some changes of 'delete' or 'rm' to 'unlink'. refs #1104
3068david-sarah@jacaranda.org**20110713002722
3069 Ignore-this: 304d2a330d5e6e77d5f1feed7814b21c
3070]
3071[WUI: change the label of the button to unlink a file from 'del' to 'unlink'. Also change some internal names to 'unlink', and allow 't=unlink' as a synonym for 't=delete' in the web-API interface. Incidentally, improve a test to check for the rename button as well as the unlink button. fixes #1104
3072david-sarah@jacaranda.org**20110713001218
3073 Ignore-this: 3eef6b3f81b94a9c0020a38eb20aa069
3074]
3075[src/allmydata/web/filenode.py: delete a stale comment that was made incorrect by changeset [3133].
3076david-sarah@jacaranda.org**20110801203009
3077 Ignore-this: b3912e95a874647027efdc97822dd10e
3078]
3079[fix typo introduced during rebasing of 'remove get_serverid from
3080Brian Warner <warner@lothar.com>**20110801200341
3081 Ignore-this: 4235b0f585c0533892193941dbbd89a8
3082 DownloadStatus.add_dyhb_request and customers' patch, to fix test failure.
3083]
3084[remove get_serverid from DownloadStatus.add_dyhb_request and customers
3085zooko@zooko.com**20110801185401
3086 Ignore-this: db188c18566d2d0ab39a80c9dc8f6be6
3087 This patch is a rebase of a patch originally written by Brian. I didn't change any of the intent of Brian's patch, just ported it to current trunk.
3088 refs #1363
3089]
3090[remove get_serverid from DownloadStatus.add_block_request and customers
3091zooko@zooko.com**20110801185344
3092 Ignore-this: 8bfa8201d6147f69b0fbe31beea9c1e
3093 This is a rebase of a patch Brian originally wrote. I haven't changed the intent of that patch, just ported it to trunk.
3094 refs #1363
3095]
3096[apply zooko's advice: storage_client get_known_servers() returns a frozenset, caller sorts
3097warner@lothar.com**20110801174452
3098 Ignore-this: 2aa13ea6cbed4e9084bd604bf8633692
3099 refs #1363
3100]
3101[test_immutable.Test: rewrite to use NoNetworkGrid, now takes 2.7s not 97s
3102warner@lothar.com**20110801174444
3103 Ignore-this: 54f30b5d7461d2b3514e2a0172f3a98c
3104 remove now-unused ShareManglingMixin
3105 refs #1363
3106]
3107[DownloadStatus.add_known_share wants to be used by Finder, web.status
3108warner@lothar.com**20110801174436
3109 Ignore-this: 1433bcd73099a579abe449f697f35f9
3110 refs #1363
3111]
3112[replace IServer.name() with get_name(), and get_longname()
3113warner@lothar.com**20110801174428
3114 Ignore-this: e5a6f7f6687fd7732ddf41cfdd7c491b
3115 
3116 This patch was originally written by Brian, but was re-recorded by Zooko to use
3117 darcs replace instead of hunks for any file in which it would result in fewer
3118 total hunks.
3119 refs #1363
3120]
3121[upload.py: apply David-Sarah's advice rename (un)contacted(2) trackers to first_pass/second_pass/next_pass
3122zooko@zooko.com**20110801174143
3123 Ignore-this: e36e1420bba0620a0107bd90032a5198
3124 This patch was written by Brian but was re-recorded by Zooko (with David-Sarah looking on) to use darcs replace instead of editing to rename the three variables to their new names.
3125 refs #1363
3126]
3127[Coalesce multiple Share.loop() calls, make downloads faster. Closes #1268.
3128Brian Warner <warner@lothar.com>**20110801151834
3129 Ignore-this: 48530fce36c01c0ff708f61c2de7e67a
3130]
3131[src/allmydata/_auto_deps.py: 'i686' is another way of spelling x86.
3132david-sarah@jacaranda.org**20110801034035
3133 Ignore-this: 6971e0621db2fba794d86395b4d51038
3134]
3135[tahoe_rm.py: better error message when there is no path. refs #1292
3136david-sarah@jacaranda.org**20110122064212
3137 Ignore-this: ff3bb2c9f376250e5fd77eb009e09018
3138]
3139[test_cli.py: Test for error message when 'tahoe rm' is invoked without a path. refs #1292
3140david-sarah@jacaranda.org**20110104105108
3141 Ignore-this: 29ec2f2e0251e446db96db002ad5dd7d
3142]
3143[src/allmydata/__init__.py: suppress a spurious warning from 'bin/tahoe --version[-and-path]' about twisted-web and twisted-core packages.
3144david-sarah@jacaranda.org**20110801005209
3145 Ignore-this: 50e7cd53cca57b1870d9df0361c7c709
3146]
3147[test_cli.py: use to_str on fields loaded using simplejson.loads in new tests. refs #1304
3148david-sarah@jacaranda.org**20110730032521
3149 Ignore-this: d1d6dfaefd1b4e733181bf127c79c00b
3150]
3151[cli: make 'tahoe cp' overwrite mutable files in-place
3152Kevan Carstensen <kevan@isnotajoke.com>**20110729202039
3153 Ignore-this: b2ad21a19439722f05c49bfd35b01855
3154]
3155[SFTP: write an error message to standard error for unrecognized shell commands. Change the existing message for shell sessions to be written to standard error, and refactor some duplicated code. Also change the lines of the error messages to end in CRLF, and take into account Kevan's review comments. fixes #1442, #1446
3156david-sarah@jacaranda.org**20110729233102
3157 Ignore-this: d2f2bb4664f25007d1602bf7333e2cdd
3158]
3159[src/allmydata/scripts/cli.py: fix pyflakes warning.
3160david-sarah@jacaranda.org**20110728021402
3161 Ignore-this: 94050140ddb99865295973f49927c509
3162]
3163[Fix the help synopses of CLI commands to include [options] in the right place. fixes #1359, fixes #636
3164david-sarah@jacaranda.org**20110724225440
3165 Ignore-this: 2a8e488a5f63dabfa9db9efd83768a5
3166]
3167[encodingutil: argv and output encodings are always the same on all platforms. Lose the unnecessary generality of them being different. fixes #1120
3168david-sarah@jacaranda.org**20110629185356
3169 Ignore-this: 5ebacbe6903dfa83ffd3ff8436a97787
3170]
3171[docs/man/tahoe.1: add man page. fixes #1420
3172david-sarah@jacaranda.org**20110724171728
3173 Ignore-this: fc7601ec7f25494288d6141d0ae0004c
3174]
3175[Update the dependency on zope.interface to fix an incompatiblity between Nevow and zope.interface 3.6.4. fixes #1435
3176david-sarah@jacaranda.org**20110721234941
3177 Ignore-this: 2ff3fcfc030fca1a4d4c7f1fed0f2aa9
3178]
3179[frontends/ftpd.py: remove the check for IWriteFile.close since we're now guaranteed to be using Twisted >= 10.1 which has it.
3180david-sarah@jacaranda.org**20110722000320
3181 Ignore-this: 55cd558b791526113db3f83c00ec328a
3182]
3183[Update the dependency on Twisted to >= 10.1. This allows us to simplify some documentation: it's no longer necessary to install pywin32 on Windows, or apply a patch to Twisted in order to use the FTP frontend. fixes #1274, #1438. refs #1429
3184david-sarah@jacaranda.org**20110721233658
3185 Ignore-this: 81b41745477163c9b39c0b59db91cc62
3186]
3187[misc/build_helpers/run_trial.py: undo change to block pywin32 (it didn't work because run_trial.py is no longer used). refs #1334
3188david-sarah@jacaranda.org**20110722035402
3189 Ignore-this: 5d03f544c4154f088e26c7107494bf39
3190]
3191[misc/build_helpers/run_trial.py: ensure that pywin32 is not on the sys.path when running the test suite. Includes some temporary debugging printouts that will be removed. refs #1334
3192david-sarah@jacaranda.org**20110722024907
3193 Ignore-this: 5141a9f83a4085ed4ca21f0bbb20bb9c
3194]
3195[docs/running.rst: use 'tahoe run ~/.tahoe' instead of 'tahoe run' (the default is the current directory, unlike 'tahoe start').
3196david-sarah@jacaranda.org**20110718005949
3197 Ignore-this: 81837fbce073e93d88a3e7ae3122458c
3198]
3199[docs/running.rst: say to put the introducer.furl in tahoe.cfg.
3200david-sarah@jacaranda.org**20110717194315
3201 Ignore-this: 954cc4c08e413e8c62685d58ff3e11f3
3202]
3203[README.txt: say that quickstart.rst is in the docs directory.
3204david-sarah@jacaranda.org**20110717192400
3205 Ignore-this: bc6d35a85c496b77dbef7570677ea42a
3206]
3207[setup: remove the dependency on foolscap's "secure_connections" extra, add a dependency on pyOpenSSL
3208zooko@zooko.com**20110717114226
3209 Ignore-this: df222120d41447ce4102616921626c82
3210 fixes #1383
3211]
3212[test_sftp.py cleanup: remove a redundant definition of failUnlessReallyEqual.
3213david-sarah@jacaranda.org**20110716181813
3214 Ignore-this: 50113380b368c573f07ac6fe2eb1e97f
3215]
3216[docs: add missing link in NEWS.rst
3217zooko@zooko.com**20110712153307
3218 Ignore-this: be7b7eb81c03700b739daa1027d72b35
3219]
3220[contrib: remove the contributed fuse modules and the entire contrib/ directory, which is now empty
3221zooko@zooko.com**20110712153229
3222 Ignore-this: 723c4f9e2211027c79d711715d972c5
3223 Also remove a couple of vestigial references to figleaf, which is long gone.
3224 fixes #1409 (remove contrib/fuse)
3225]
3226[add Protovis.js-based download-status timeline visualization
3227Brian Warner <warner@lothar.com>**20110629222606
3228 Ignore-this: 477ccef5c51b30e246f5b6e04ab4a127
3229 
3230 provide status overlap info on the webapi t=json output, add decode/decrypt
3231 rate tooltips, add zoomin/zoomout buttons
3232]
3233[add more download-status data, fix tests
3234Brian Warner <warner@lothar.com>**20110629222555
3235 Ignore-this: e9e0b7e0163f1e95858aa646b9b17b8c
3236]
3237[prepare for viz: improve DownloadStatus events
3238Brian Warner <warner@lothar.com>**20110629222542
3239 Ignore-this: 16d0bde6b734bb501aa6f1174b2b57be
3240 
3241 consolidate IDownloadStatusHandlingConsumer stuff into DownloadNode
3242]
3243[docs: fix error in crypto specification that was noticed by Taylor R Campbell <campbell+tahoe@mumble.net>
3244zooko@zooko.com**20110629185711
3245 Ignore-this: b921ed60c1c8ba3c390737fbcbe47a67
3246]
3247[setup.py: don't make bin/tahoe.pyscript executable. fixes #1347
3248david-sarah@jacaranda.org**20110130235809
3249 Ignore-this: 3454c8b5d9c2c77ace03de3ef2d9398a
3250]
3251[Makefile: remove targets relating to 'setup.py check_auto_deps' which no longer exists. fixes #1345
3252david-sarah@jacaranda.org**20110626054124
3253 Ignore-this: abb864427a1b91bd10d5132b4589fd90
3254]
3255[Makefile: add 'make check' as an alias for 'make test'. Also remove an unnecessary dependency of 'test' on 'build' and 'src/allmydata/_version.py'. fixes #1344
3256david-sarah@jacaranda.org**20110623205528
3257 Ignore-this: c63e23146c39195de52fb17c7c49b2da
3258]
3259[Rename test_package_initialization.py to (much shorter) test_import.py .
3260Brian Warner <warner@lothar.com>**20110611190234
3261 Ignore-this: 3eb3dbac73600eeff5cfa6b65d65822
3262 
3263 The former name was making my 'ls' listings hard to read, by forcing them
3264 down to just two columns.
3265]
3266[tests: fix tests to accomodate [20110611153758-92b7f-0ba5e4726fb6318dac28fb762a6512a003f4c430]
3267zooko@zooko.com**20110611163741
3268 Ignore-this: 64073a5f39e7937e8e5e1314c1a302d1
3269 Apparently none of the two authors (stercor, terrell), three reviewers (warner, davidsarah, terrell), or one committer (me) actually ran the tests. This is presumably due to #20.
3270 fixes #1412
3271]
3272[wui: right-align the size column in the WUI
3273zooko@zooko.com**20110611153758
3274 Ignore-this: 492bdaf4373c96f59f90581c7daf7cd7
3275 Thanks to Ted "stercor" Rolle Jr. and Terrell Russell.
3276 fixes #1412
3277]
3278[docs: three minor fixes
3279zooko@zooko.com**20110610121656
3280 Ignore-this: fec96579eb95aceb2ad5fc01a814c8a2
3281 CREDITS for arc for stats tweak
3282 fix link to .zip file in quickstart.rst (thanks to ChosenOne for noticing)
3283 English usage tweak
3284]
3285[docs/running.rst: fix stray HTML (not .rst) link noticed by ChosenOne.
3286david-sarah@jacaranda.org**20110609223719
3287 Ignore-this: fc50ac9c94792dcac6f1067df8ac0d4a
3288]
3289[server.py:  get_latencies now reports percentiles _only_ if there are sufficient observations for the interpretation of the percentile to be unambiguous.
3290wilcoxjg@gmail.com**20110527120135
3291 Ignore-this: 2e7029764bffc60e26f471d7c2b6611e
3292 interfaces.py:  modified the return type of RIStatsProvider.get_stats to allow for None as a return value
3293 NEWS.rst, stats.py: documentation of change to get_latencies
3294 stats.rst: now documents percentile modification in get_latencies
3295 test_storage.py:  test_latencies now expects None in output categories that contain too few samples for the associated percentile to be unambiguously reported.
3296 fixes #1392
3297]
3298[docs: revert link in relnotes.txt from NEWS.rst to NEWS, since the former did not exist at revision 5000.
3299david-sarah@jacaranda.org**20110517011214
3300 Ignore-this: 6a5be6e70241e3ec0575641f64343df7
3301]
3302[docs: convert NEWS to NEWS.rst and change all references to it.
3303david-sarah@jacaranda.org**20110517010255
3304 Ignore-this: a820b93ea10577c77e9c8206dbfe770d
3305]
3306[docs: remove out-of-date docs/testgrid/introducer.furl and containing directory. fixes #1404
3307david-sarah@jacaranda.org**20110512140559
3308 Ignore-this: 784548fc5367fac5450df1c46890876d
3309]
3310[scripts/common.py: don't assume that the default alias is always 'tahoe' (it is, but the API of get_alias doesn't say so). refs #1342
3311david-sarah@jacaranda.org**20110130164923
3312 Ignore-this: a271e77ce81d84bb4c43645b891d92eb
3313]
3314[setup: don't catch all Exception from check_requirement(), but only PackagingError and ImportError
3315zooko@zooko.com**20110128142006
3316 Ignore-this: 57d4bc9298b711e4bc9dc832c75295de
3317 I noticed this because I had accidentally inserted a bug which caused AssertionError to be raised from check_requirement().
3318]
3319[M-x whitespace-cleanup
3320zooko@zooko.com**20110510193653
3321 Ignore-this: dea02f831298c0f65ad096960e7df5c7
3322]
3323[docs: fix typo in running.rst, thanks to arch_o_median
3324zooko@zooko.com**20110510193633
3325 Ignore-this: ca06de166a46abbc61140513918e79e8
3326]
3327[relnotes.txt: don't claim to work on Cygwin (which has been untested for some time). refs #1342
3328david-sarah@jacaranda.org**20110204204902
3329 Ignore-this: 85ef118a48453d93fa4cddc32d65b25b
3330]
3331[relnotes.txt: forseeable -> foreseeable. refs #1342
3332david-sarah@jacaranda.org**20110204204116
3333 Ignore-this: 746debc4d82f4031ebf75ab4031b3a9
3334]
3335[replace remaining .html docs with .rst docs
3336zooko@zooko.com**20110510191650
3337 Ignore-this: d557d960a986d4ac8216d1677d236399
3338 Remove install.html (long since deprecated).
3339 Also replace some obsolete references to install.html with references to quickstart.rst.
3340 Fix some broken internal references within docs/historical/historical_known_issues.txt.
3341 Thanks to Ravi Pinjala and Patrick McDonald.
3342 refs #1227
3343]
3344[docs: FTP-and-SFTP.rst: fix a minor error and update the information about which version of Twisted fixes #1297
3345zooko@zooko.com**20110428055232
3346 Ignore-this: b63cfb4ebdbe32fb3b5f885255db4d39
3347]
3348[munin tahoe_files plugin: fix incorrect file count
3349francois@ctrlaltdel.ch**20110428055312
3350 Ignore-this: 334ba49a0bbd93b4a7b06a25697aba34
3351 fixes #1391
3352]
3353[corrected "k must never be smaller than N" to "k must never be greater than N"
3354secorp@allmydata.org**20110425010308
3355 Ignore-this: 233129505d6c70860087f22541805eac
3356]
3357[Fix a test failure in test_package_initialization on Python 2.4.x due to exceptions being stringified differently than in later versions of Python. refs #1389
3358david-sarah@jacaranda.org**20110411190738
3359 Ignore-this: 7847d26bc117c328c679f08a7baee519
3360]
3361[tests: add test for including the ImportError message and traceback entry in the summary of errors from importing dependencies. refs #1389
3362david-sarah@jacaranda.org**20110410155844
3363 Ignore-this: fbecdbeb0d06a0f875fe8d4030aabafa
3364]
3365[allmydata/__init__.py: preserve the message and last traceback entry (file, line number, function, and source line) of ImportErrors in the package versions string. fixes #1389
3366david-sarah@jacaranda.org**20110410155705
3367 Ignore-this: 2f87b8b327906cf8bfca9440a0904900
3368]
3369[remove unused variable detected by pyflakes
3370zooko@zooko.com**20110407172231
3371 Ignore-this: 7344652d5e0720af822070d91f03daf9
3372]
3373[allmydata/__init__.py: Nicer reporting of unparseable version numbers in dependencies. fixes #1388
3374david-sarah@jacaranda.org**20110401202750
3375 Ignore-this: 9c6bd599259d2405e1caadbb3e0d8c7f
3376]
3377[update FTP-and-SFTP.rst: the necessary patch is included in Twisted-10.1
3378Brian Warner <warner@lothar.com>**20110325232511
3379 Ignore-this: d5307faa6900f143193bfbe14e0f01a
3380]
3381[control.py: remove all uses of s.get_serverid()
3382warner@lothar.com**20110227011203
3383 Ignore-this: f80a787953bd7fa3d40e828bde00e855
3384]
3385[web: remove some uses of s.get_serverid(), not all
3386warner@lothar.com**20110227011159
3387 Ignore-this: a9347d9cf6436537a47edc6efde9f8be
3388]
3389[immutable/downloader/fetcher.py: remove all get_serverid() calls
3390warner@lothar.com**20110227011156
3391 Ignore-this: fb5ef018ade1749348b546ec24f7f09a
3392]
3393[immutable/downloader/fetcher.py: fix diversity bug in server-response handling
3394warner@lothar.com**20110227011153
3395 Ignore-this: bcd62232c9159371ae8a16ff63d22c1b
3396 
3397 When blocks terminate (either COMPLETE or CORRUPT/DEAD/BADSEGNUM), the
3398 _shares_from_server dict was being popped incorrectly (using shnum as the
3399 index instead of serverid). I'm still thinking through the consequences of
3400 this bug. It was probably benign and really hard to detect. I think it would
3401 cause us to incorrectly believe that we're pulling too many shares from a
3402 server, and thus prefer a different server rather than asking for a second
3403 share from the first server. The diversity code is intended to spread out the
3404 number of shares simultaneously being requested from each server, but with
3405 this bug, it might be spreading out the total number of shares requested at
3406 all, not just simultaneously. (note that SegmentFetcher is scoped to a single
3407 segment, so the effect doesn't last very long).
3408]
3409[immutable/downloader/share.py: reduce get_serverid(), one left, update ext deps
3410warner@lothar.com**20110227011150
3411 Ignore-this: d8d56dd8e7b280792b40105e13664554
3412 
3413 test_download.py: create+check MyShare instances better, make sure they share
3414 Server objects, now that finder.py cares
3415]
3416[immutable/downloader/finder.py: reduce use of get_serverid(), one left
3417warner@lothar.com**20110227011146
3418 Ignore-this: 5785be173b491ae8a78faf5142892020
3419]
3420[immutable/offloaded.py: reduce use of get_serverid() a bit more
3421warner@lothar.com**20110227011142
3422 Ignore-this: b48acc1b2ae1b311da7f3ba4ffba38f
3423]
3424[immutable/upload.py: reduce use of get_serverid()
3425warner@lothar.com**20110227011138
3426 Ignore-this: ffdd7ff32bca890782119a6e9f1495f6
3427]
3428[immutable/checker.py: remove some uses of s.get_serverid(), not all
3429warner@lothar.com**20110227011134
3430 Ignore-this: e480a37efa9e94e8016d826c492f626e
3431]
3432[add remaining get_* methods to storage_client.Server, NoNetworkServer, and
3433warner@lothar.com**20110227011132
3434 Ignore-this: 6078279ddf42b179996a4b53bee8c421
3435 MockIServer stubs
3436]
3437[upload.py: rearrange _make_trackers a bit, no behavior changes
3438warner@lothar.com**20110227011128
3439 Ignore-this: 296d4819e2af452b107177aef6ebb40f
3440]
3441[happinessutil.py: finally rename merge_peers to merge_servers
3442warner@lothar.com**20110227011124
3443 Ignore-this: c8cd381fea1dd888899cb71e4f86de6e
3444]
3445[test_upload.py: factor out FakeServerTracker
3446warner@lothar.com**20110227011120
3447 Ignore-this: 6c182cba90e908221099472cc159325b
3448]
3449[test_upload.py: server-vs-tracker cleanup
3450warner@lothar.com**20110227011115
3451 Ignore-this: 2915133be1a3ba456e8603885437e03
3452]
3453[happinessutil.py: server-vs-tracker cleanup
3454warner@lothar.com**20110227011111
3455 Ignore-this: b856c84033562d7d718cae7cb01085a9
3456]
3457[upload.py: more tracker-vs-server cleanup
3458warner@lothar.com**20110227011107
3459 Ignore-this: bb75ed2afef55e47c085b35def2de315
3460]
3461[upload.py: fix var names to avoid confusion between 'trackers' and 'servers'
3462warner@lothar.com**20110227011103
3463 Ignore-this: 5d5e3415b7d2732d92f42413c25d205d
3464]
3465[refactor: s/peer/server/ in immutable/upload, happinessutil.py, test_upload
3466warner@lothar.com**20110227011100
3467 Ignore-this: 7ea858755cbe5896ac212a925840fe68
3468 
3469 No behavioral changes, just updating variable/method names and log messages.
3470 The effects outside these three files should be minimal: some exception
3471 messages changed (to say "server" instead of "peer"), and some internal class
3472 names were changed. A few things still use "peer" to minimize external
3473 changes, like UploadResults.timings["peer_selection"] and
3474 happinessutil.merge_peers, which can be changed later.
3475]
3476[storage_client.py: clean up test_add_server/test_add_descriptor, remove .test_servers
3477warner@lothar.com**20110227011056
3478 Ignore-this: efad933e78179d3d5fdcd6d1ef2b19cc
3479]
3480[test_client.py, upload.py:: remove KiB/MiB/etc constants, and other dead code
3481warner@lothar.com**20110227011051
3482 Ignore-this: dc83c5794c2afc4f81e592f689c0dc2d
3483]
3484[test: increase timeout on a network test because Francois's ARM machine hit that timeout
3485zooko@zooko.com**20110317165909
3486 Ignore-this: 380c345cdcbd196268ca5b65664ac85b
3487 I'm skeptical that the test was proceeding correctly but ran out of time. It seems more likely that it had gotten hung. But if we raise the timeout to an even more extravagant number then we can be even more certain that the test was never going to finish.
3488]
3489[docs/configuration.rst: add a "Frontend Configuration" section
3490Brian Warner <warner@lothar.com>**20110222014323
3491 Ignore-this: 657018aa501fe4f0efef9851628444ca
3492 
3493 this points to docs/frontends/*.rst, which were previously underlinked
3494]
3495[web/filenode.py: avoid calling req.finish() on closed HTTP connections. Closes #1366
3496"Brian Warner <warner@lothar.com>"**20110221061544
3497 Ignore-this: 799d4de19933f2309b3c0c19a63bb888
3498]
3499[Add unit tests for cross_check_pkg_resources_versus_import, and a regression test for ref #1355. This requires a little refactoring to make it testable.
3500david-sarah@jacaranda.org**20110221015817
3501 Ignore-this: 51d181698f8c20d3aca58b057e9c475a
3502]
3503[allmydata/__init__.py: .name was used in place of the correct .__name__ when printing an exception. Also, robustify string formatting by using %r instead of %s in some places. fixes #1355.
3504david-sarah@jacaranda.org**20110221020125
3505 Ignore-this: b0744ed58f161bf188e037bad077fc48
3506]
3507[Refactor StorageFarmBroker handling of servers
3508Brian Warner <warner@lothar.com>**20110221015804
3509 Ignore-this: 842144ed92f5717699b8f580eab32a51
3510 
3511 Pass around IServer instance instead of (peerid, rref) tuple. Replace
3512 "descriptor" with "server". Other replacements:
3513 
3514  get_all_servers -> get_connected_servers/get_known_servers
3515  get_servers_for_index -> get_servers_for_psi (now returns IServers)
3516 
3517 This change still needs to be pushed further down: lots of code is now
3518 getting the IServer and then distributing (peerid, rref) internally.
3519 Instead, it ought to distribute the IServer internally and delay
3520 extracting a serverid or rref until the last moment.
3521 
3522 no_network.py was updated to retain parallelism.
3523]
3524[TAG allmydata-tahoe-1.8.2
3525warner@lothar.com**20110131020101]
3526Patch bundle hash:
3527a4d8fe736c5d92874e732666a2c2e30a04631522