Ticket #999: checkpoint3.darcs.patch

File checkpoint3.darcs.patch, 97.0 KB (added by arch_o_median, at 2011-06-26T17:11:13Z)

another checkpoint

Line 
1Fri Mar 25 14:35:14 MDT 2011  wilcoxjg@gmail.com
2  * storage: new mocking tests of storage server read and write
3  There are already tests of read and functionality in test_storage.py, but those tests let the code under test use a real filesystem whereas these tests mock all file system calls.
4
5Fri Jun 24 14:28:50 MDT 2011  wilcoxjg@gmail.com
6  * server.py, test_backends.py, interfaces.py, immutable.py (others?): working patch for implementation of backends plugin
7  sloppy not for production
8
9Sat Jun 25 23:27:32 MDT 2011  wilcoxjg@gmail.com
10  * a temp patch used as a snapshot
11
12Sat Jun 25 23:32:44 MDT 2011  wilcoxjg@gmail.com
13  * snapshot of progress on backend implementation (not suitable for trunk)
14
15Sun Jun 26 10:57:15 MDT 2011  wilcoxjg@gmail.com
16  * checkpoint patch
17
18New patches:
19
20[storage: new mocking tests of storage server read and write
21wilcoxjg@gmail.com**20110325203514
22 Ignore-this: df65c3c4f061dd1516f88662023fdb41
23 There are already tests of read and functionality in test_storage.py, but those tests let the code under test use a real filesystem whereas these tests mock all file system calls.
24] {
25addfile ./src/allmydata/test/test_server.py
26hunk ./src/allmydata/test/test_server.py 1
27+from twisted.trial import unittest
28+
29+from StringIO import StringIO
30+
31+from allmydata.test.common_util import ReallyEqualMixin
32+
33+import mock
34+
35+# This is the code that we're going to be testing.
36+from allmydata.storage.server import StorageServer
37+
38+# The following share file contents was generated with
39+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
40+# with share data == 'a'.
41+share_data = 'a\x00\x00\x00\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\x00(\xde\x80'
42+share_file_data = '\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01' + share_data
43+
44+sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
45+
46+class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
47+    @mock.patch('__builtin__.open')
48+    def test_create_server(self, mockopen):
49+        """ This tests whether a server instance can be constructed. """
50+
51+        def call_open(fname, mode):
52+            if fname == 'testdir/bucket_counter.state':
53+                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
54+            elif fname == 'testdir/lease_checker.state':
55+                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
56+            elif fname == 'testdir/lease_checker.history':
57+                return StringIO()
58+        mockopen.side_effect = call_open
59+
60+        # Now begin the test.
61+        s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
62+
63+        # You passed!
64+
65+class TestServer(unittest.TestCase, ReallyEqualMixin):
66+    @mock.patch('__builtin__.open')
67+    def setUp(self, mockopen):
68+        def call_open(fname, mode):
69+            if fname == 'testdir/bucket_counter.state':
70+                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
71+            elif fname == 'testdir/lease_checker.state':
72+                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
73+            elif fname == 'testdir/lease_checker.history':
74+                return StringIO()
75+        mockopen.side_effect = call_open
76+
77+        self.s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
78+
79+
80+    @mock.patch('time.time')
81+    @mock.patch('os.mkdir')
82+    @mock.patch('__builtin__.open')
83+    @mock.patch('os.listdir')
84+    @mock.patch('os.path.isdir')
85+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
86+        """Handle a report of corruption."""
87+
88+        def call_listdir(dirname):
89+            self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
90+            raise OSError(2, "No such file or directory: 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a'")
91+
92+        mocklistdir.side_effect = call_listdir
93+
94+        class MockFile:
95+            def __init__(self):
96+                self.buffer = ''
97+                self.pos = 0
98+            def write(self, instring):
99+                begin = self.pos
100+                padlen = begin - len(self.buffer)
101+                if padlen > 0:
102+                    self.buffer += '\x00' * padlen
103+                end = self.pos + len(instring)
104+                self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
105+                self.pos = end
106+            def close(self):
107+                pass
108+            def seek(self, pos):
109+                self.pos = pos
110+            def read(self, numberbytes):
111+                return self.buffer[self.pos:self.pos+numberbytes]
112+            def tell(self):
113+                return self.pos
114+
115+        mocktime.return_value = 0
116+
117+        sharefile = MockFile()
118+        def call_open(fname, mode):
119+            self.failUnlessReallyEqual(fname, 'testdir/shares/incoming/or/orsxg5dtorxxeylhmvpws3temv4a/0' )
120+            return sharefile
121+
122+        mockopen.side_effect = call_open
123+        # Now begin the test.
124+        alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
125+        print bs
126+        bs[0].remote_write(0, 'a')
127+        self.failUnlessReallyEqual(sharefile.buffer, share_file_data)
128+
129+
130+    @mock.patch('os.path.exists')
131+    @mock.patch('os.path.getsize')
132+    @mock.patch('__builtin__.open')
133+    @mock.patch('os.listdir')
134+    def test_read_share(self, mocklistdir, mockopen, mockgetsize, mockexists):
135+        """ This tests whether the code correctly finds and reads
136+        shares written out by old (Tahoe-LAFS <= v1.8.2)
137+        servers. There is a similar test in test_download, but that one
138+        is from the perspective of the client and exercises a deeper
139+        stack of code. This one is for exercising just the
140+        StorageServer object. """
141+
142+        def call_listdir(dirname):
143+            self.failUnlessReallyEqual(dirname,'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
144+            return ['0']
145+
146+        mocklistdir.side_effect = call_listdir
147+
148+        def call_open(fname, mode):
149+            self.failUnlessReallyEqual(fname, sharefname)
150+            self.failUnless('r' in mode, mode)
151+            self.failUnless('b' in mode, mode)
152+
153+            return StringIO(share_file_data)
154+        mockopen.side_effect = call_open
155+
156+        datalen = len(share_file_data)
157+        def call_getsize(fname):
158+            self.failUnlessReallyEqual(fname, sharefname)
159+            return datalen
160+        mockgetsize.side_effect = call_getsize
161+
162+        def call_exists(fname):
163+            self.failUnlessReallyEqual(fname, sharefname)
164+            return True
165+        mockexists.side_effect = call_exists
166+
167+        # Now begin the test.
168+        bs = self.s.remote_get_buckets('teststorage_index')
169+
170+        self.failUnlessEqual(len(bs), 1)
171+        b = bs[0]
172+        self.failUnlessReallyEqual(b.remote_read(0, datalen), share_data)
173+        # If you try to read past the end you get the as much data as is there.
174+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), share_data)
175+        # If you start reading past the end of the file you get the empty string.
176+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
177}
178[server.py, test_backends.py, interfaces.py, immutable.py (others?): working patch for implementation of backends plugin
179wilcoxjg@gmail.com**20110624202850
180 Ignore-this: ca6f34987ee3b0d25cac17c1fc22d50c
181 sloppy not for production
182] {
183move ./src/allmydata/test/test_server.py ./src/allmydata/test/test_backends.py
184hunk ./src/allmydata/storage/crawler.py 13
185     pass
186 
187 class ShareCrawler(service.MultiService):
188-    """A ShareCrawler subclass is attached to a StorageServer, and
189+    """A subcless of ShareCrawler is attached to a StorageServer, and
190     periodically walks all of its shares, processing each one in some
191     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
192     since large servers can easily have a terabyte of shares, in several
193hunk ./src/allmydata/storage/crawler.py 31
194     We assume that the normal upload/download/get_buckets traffic of a tahoe
195     grid will cause the prefixdir contents to be mostly cached in the kernel,
196     or that the number of buckets in each prefixdir will be small enough to
197-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
198+    load quickly. A 1TB allmydata.com server was measured to have 2.56 * 10^6
199     buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
200     prefix. On this server, each prefixdir took 130ms-200ms to list the first
201     time, and 17ms to list the second time.
202hunk ./src/allmydata/storage/crawler.py 68
203     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
204     minimum_cycle_time = 300 # don't run a cycle faster than this
205 
206-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
207+    def __init__(self, backend, statefile, allowed_cpu_percentage=None):
208         service.MultiService.__init__(self)
209         if allowed_cpu_percentage is not None:
210             self.allowed_cpu_percentage = allowed_cpu_percentage
211hunk ./src/allmydata/storage/crawler.py 72
212-        self.server = server
213-        self.sharedir = server.sharedir
214-        self.statefile = statefile
215+        self.backend = backend
216         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
217                          for i in range(2**10)]
218         self.prefixes.sort()
219hunk ./src/allmydata/storage/crawler.py 446
220 
221     minimum_cycle_time = 60*60 # we don't need this more than once an hour
222 
223-    def __init__(self, server, statefile, num_sample_prefixes=1):
224-        ShareCrawler.__init__(self, server, statefile)
225+    def __init__(self, statefile, num_sample_prefixes=1):
226+        ShareCrawler.__init__(self, statefile)
227         self.num_sample_prefixes = num_sample_prefixes
228 
229     def add_initial_state(self):
230hunk ./src/allmydata/storage/expirer.py 15
231     removed.
232 
233     I collect statistics on the leases and make these available to a web
234-    status page, including::
235+    status page, including:
236 
237     Space recovered during this cycle-so-far:
238      actual (only if expiration_enabled=True):
239hunk ./src/allmydata/storage/expirer.py 51
240     slow_start = 360 # wait 6 minutes after startup
241     minimum_cycle_time = 12*60*60 # not more than twice per day
242 
243-    def __init__(self, server, statefile, historyfile,
244+    def __init__(self, statefile, historyfile,
245                  expiration_enabled, mode,
246                  override_lease_duration, # used if expiration_mode=="age"
247                  cutoff_date, # used if expiration_mode=="cutoff-date"
248hunk ./src/allmydata/storage/expirer.py 71
249         else:
250             raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
251         self.sharetypes_to_expire = sharetypes
252-        ShareCrawler.__init__(self, server, statefile)
253+        ShareCrawler.__init__(self, statefile)
254 
255     def add_initial_state(self):
256         # we fill ["cycle-to-date"] here (even though they will be reset in
257hunk ./src/allmydata/storage/immutable.py 44
258     sharetype = "immutable"
259 
260     def __init__(self, filename, max_size=None, create=False):
261-        """ 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. """
262+        """ If max_size is not None then I won't allow more than
263+        max_size to be written to me. If create=True then max_size
264+        must not be None. """
265         precondition((max_size is not None) or (not create), max_size, create)
266         self.home = filename
267         self._max_size = max_size
268hunk ./src/allmydata/storage/immutable.py 87
269 
270     def read_share_data(self, offset, length):
271         precondition(offset >= 0)
272-        # reads beyond the end of the data are truncated. Reads that start
273-        # beyond the end of the data return an empty string. I wonder why
274-        # Python doesn't do the following computation for me?
275+        # Reads beyond the end of the data are truncated. Reads that start
276+        # beyond the end of the data return an empty string.
277         seekpos = self._data_offset+offset
278         fsize = os.path.getsize(self.home)
279         actuallength = max(0, min(length, fsize-seekpos))
280hunk ./src/allmydata/storage/immutable.py 198
281             space_freed += os.stat(self.home)[stat.ST_SIZE]
282             self.unlink()
283         return space_freed
284+class NullBucketWriter(Referenceable):
285+    implements(RIBucketWriter)
286 
287hunk ./src/allmydata/storage/immutable.py 201
288+    def remote_write(self, offset, data):
289+        return
290 
291 class BucketWriter(Referenceable):
292     implements(RIBucketWriter)
293hunk ./src/allmydata/storage/server.py 7
294 from twisted.application import service
295 
296 from zope.interface import implements
297-from allmydata.interfaces import RIStorageServer, IStatsProducer
298+from allmydata.interfaces import RIStorageServer, IStatsProducer, IShareStore
299 from allmydata.util import fileutil, idlib, log, time_format
300 import allmydata # for __full_version__
301 
302hunk ./src/allmydata/storage/server.py 16
303 from allmydata.storage.lease import LeaseInfo
304 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
305      create_mutable_sharefile
306-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
307+from allmydata.storage.immutable import ShareFile, NullBucketWriter, BucketWriter, BucketReader
308 from allmydata.storage.crawler import BucketCountingCrawler
309 from allmydata.storage.expirer import LeaseCheckingCrawler
310 
311hunk ./src/allmydata/storage/server.py 20
312+from zope.interface import implements
313+
314+# A Backend is a MultiService so that its server's crawlers (if the server has any) can
315+# be started and stopped.
316+class Backend(service.MultiService):
317+    implements(IStatsProducer)
318+    def __init__(self):
319+        service.MultiService.__init__(self)
320+
321+    def get_bucket_shares(self):
322+        """XXX"""
323+        raise NotImplementedError
324+
325+    def get_share(self):
326+        """XXX"""
327+        raise NotImplementedError
328+
329+    def make_bucket_writer(self):
330+        """XXX"""
331+        raise NotImplementedError
332+
333+class NullBackend(Backend):
334+    def __init__(self):
335+        Backend.__init__(self)
336+
337+    def get_available_space(self):
338+        return None
339+
340+    def get_bucket_shares(self, storage_index):
341+        return set()
342+
343+    def get_share(self, storage_index, sharenum):
344+        return None
345+
346+    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
347+        return NullBucketWriter()
348+
349+class FSBackend(Backend):
350+    def __init__(self, storedir, readonly=False, reserved_space=0):
351+        Backend.__init__(self)
352+
353+        self._setup_storage(storedir, readonly, reserved_space)
354+        self._setup_corruption_advisory()
355+        self._setup_bucket_counter()
356+        self._setup_lease_checkerf()
357+
358+    def _setup_storage(self, storedir, readonly, reserved_space):
359+        self.storedir = storedir
360+        self.readonly = readonly
361+        self.reserved_space = int(reserved_space)
362+        if self.reserved_space:
363+            if self.get_available_space() is None:
364+                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",
365+                        umid="0wZ27w", level=log.UNUSUAL)
366+
367+        self.sharedir = os.path.join(self.storedir, "shares")
368+        fileutil.make_dirs(self.sharedir)
369+        self.incomingdir = os.path.join(self.sharedir, 'incoming')
370+        self._clean_incomplete()
371+
372+    def _clean_incomplete(self):
373+        fileutil.rm_dir(self.incomingdir)
374+        fileutil.make_dirs(self.incomingdir)
375+
376+    def _setup_corruption_advisory(self):
377+        # we don't actually create the corruption-advisory dir until necessary
378+        self.corruption_advisory_dir = os.path.join(self.storedir,
379+                                                    "corruption-advisories")
380+
381+    def _setup_bucket_counter(self):
382+        statefile = os.path.join(self.storedir, "bucket_counter.state")
383+        self.bucket_counter = BucketCountingCrawler(statefile)
384+        self.bucket_counter.setServiceParent(self)
385+
386+    def _setup_lease_checkerf(self):
387+        statefile = os.path.join(self.storedir, "lease_checker.state")
388+        historyfile = os.path.join(self.storedir, "lease_checker.history")
389+        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile,
390+                                   expiration_enabled, expiration_mode,
391+                                   expiration_override_lease_duration,
392+                                   expiration_cutoff_date,
393+                                   expiration_sharetypes)
394+        self.lease_checker.setServiceParent(self)
395+
396+    def get_available_space(self):
397+        if self.readonly:
398+            return 0
399+        return fileutil.get_available_space(self.storedir, self.reserved_space)
400+
401+    def get_bucket_shares(self, storage_index):
402+        """Return a list of (shnum, pathname) tuples for files that hold
403+        shares for this storage_index. In each tuple, 'shnum' will always be
404+        the integer form of the last component of 'pathname'."""
405+        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
406+        try:
407+            for f in os.listdir(storagedir):
408+                if NUM_RE.match(f):
409+                    filename = os.path.join(storagedir, f)
410+                    yield (int(f), filename)
411+        except OSError:
412+            # Commonly caused by there being no buckets at all.
413+            pass
414+
415 # storage/
416 # storage/shares/incoming
417 #   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
418hunk ./src/allmydata/storage/server.py 143
419     name = 'storage'
420     LeaseCheckerClass = LeaseCheckingCrawler
421 
422-    def __init__(self, storedir, nodeid, reserved_space=0,
423-                 discard_storage=False, readonly_storage=False,
424+    def __init__(self, nodeid, backend, reserved_space=0,
425+                 readonly_storage=False,
426                  stats_provider=None,
427                  expiration_enabled=False,
428                  expiration_mode="age",
429hunk ./src/allmydata/storage/server.py 155
430         assert isinstance(nodeid, str)
431         assert len(nodeid) == 20
432         self.my_nodeid = nodeid
433-        self.storedir = storedir
434-        sharedir = os.path.join(storedir, "shares")
435-        fileutil.make_dirs(sharedir)
436-        self.sharedir = sharedir
437-        # we don't actually create the corruption-advisory dir until necessary
438-        self.corruption_advisory_dir = os.path.join(storedir,
439-                                                    "corruption-advisories")
440-        self.reserved_space = int(reserved_space)
441-        self.no_storage = discard_storage
442-        self.readonly_storage = readonly_storage
443         self.stats_provider = stats_provider
444         if self.stats_provider:
445             self.stats_provider.register_producer(self)
446hunk ./src/allmydata/storage/server.py 158
447-        self.incomingdir = os.path.join(sharedir, 'incoming')
448-        self._clean_incomplete()
449-        fileutil.make_dirs(self.incomingdir)
450         self._active_writers = weakref.WeakKeyDictionary()
451hunk ./src/allmydata/storage/server.py 159
452+        self.backend = backend
453+        self.backend.setServiceParent(self)
454         log.msg("StorageServer created", facility="tahoe.storage")
455 
456hunk ./src/allmydata/storage/server.py 163
457-        if reserved_space:
458-            if self.get_available_space() is None:
459-                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",
460-                        umin="0wZ27w", level=log.UNUSUAL)
461-
462         self.latencies = {"allocate": [], # immutable
463                           "write": [],
464                           "close": [],
465hunk ./src/allmydata/storage/server.py 174
466                           "renew": [],
467                           "cancel": [],
468                           }
469-        self.add_bucket_counter()
470-
471-        statefile = os.path.join(self.storedir, "lease_checker.state")
472-        historyfile = os.path.join(self.storedir, "lease_checker.history")
473-        klass = self.LeaseCheckerClass
474-        self.lease_checker = klass(self, statefile, historyfile,
475-                                   expiration_enabled, expiration_mode,
476-                                   expiration_override_lease_duration,
477-                                   expiration_cutoff_date,
478-                                   expiration_sharetypes)
479-        self.lease_checker.setServiceParent(self)
480 
481     def __repr__(self):
482         return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
483hunk ./src/allmydata/storage/server.py 178
484 
485-    def add_bucket_counter(self):
486-        statefile = os.path.join(self.storedir, "bucket_counter.state")
487-        self.bucket_counter = BucketCountingCrawler(self, statefile)
488-        self.bucket_counter.setServiceParent(self)
489-
490     def count(self, name, delta=1):
491         if self.stats_provider:
492             self.stats_provider.count("storage_server." + name, delta)
493hunk ./src/allmydata/storage/server.py 233
494             kwargs["facility"] = "tahoe.storage"
495         return log.msg(*args, **kwargs)
496 
497-    def _clean_incomplete(self):
498-        fileutil.rm_dir(self.incomingdir)
499-
500     def get_stats(self):
501         # remember: RIStatsProvider requires that our return dict
502         # contains numeric values.
503hunk ./src/allmydata/storage/server.py 269
504             stats['storage_server.total_bucket_count'] = bucket_count
505         return stats
506 
507-    def get_available_space(self):
508-        """Returns available space for share storage in bytes, or None if no
509-        API to get this information is available."""
510-
511-        if self.readonly_storage:
512-            return 0
513-        return fileutil.get_available_space(self.storedir, self.reserved_space)
514-
515     def allocated_size(self):
516         space = 0
517         for bw in self._active_writers:
518hunk ./src/allmydata/storage/server.py 276
519         return space
520 
521     def remote_get_version(self):
522-        remaining_space = self.get_available_space()
523+        remaining_space = self.backend.get_available_space()
524         if remaining_space is None:
525             # We're on a platform that has no API to get disk stats.
526             remaining_space = 2**64
527hunk ./src/allmydata/storage/server.py 301
528         self.count("allocate")
529         alreadygot = set()
530         bucketwriters = {} # k: shnum, v: BucketWriter
531-        si_dir = storage_index_to_dir(storage_index)
532-        si_s = si_b2a(storage_index)
533 
534hunk ./src/allmydata/storage/server.py 302
535+        si_s = si_b2a(storage_index)
536         log.msg("storage: allocate_buckets %s" % si_s)
537 
538         # in this implementation, the lease information (including secrets)
539hunk ./src/allmydata/storage/server.py 316
540 
541         max_space_per_bucket = allocated_size
542 
543-        remaining_space = self.get_available_space()
544+        remaining_space = self.backend.get_available_space()
545         limited = remaining_space is not None
546         if limited:
547             # this is a bit conservative, since some of this allocated_size()
548hunk ./src/allmydata/storage/server.py 329
549         # they asked about: this will save them a lot of work. Add or update
550         # leases for all of them: if they want us to hold shares for this
551         # file, they'll want us to hold leases for this file.
552-        for (shnum, fn) in self._get_bucket_shares(storage_index):
553+        for (shnum, fn) in self.backend.get_bucket_shares(storage_index):
554             alreadygot.add(shnum)
555             sf = ShareFile(fn)
556             sf.add_or_renew_lease(lease_info)
557hunk ./src/allmydata/storage/server.py 335
558 
559         for shnum in sharenums:
560-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
561-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
562-            if os.path.exists(finalhome):
563+            share = self.backend.get_share(storage_index, shnum)
564+
565+            if not share:
566+                if (not limited) or (remaining_space >= max_space_per_bucket):
567+                    # ok! we need to create the new share file.
568+                    bw = self.backend.make_bucket_writer(storage_index, shnum,
569+                                      max_space_per_bucket, lease_info, canary)
570+                    bucketwriters[shnum] = bw
571+                    self._active_writers[bw] = 1
572+                    if limited:
573+                        remaining_space -= max_space_per_bucket
574+                else:
575+                    # bummer! not enough space to accept this bucket
576+                    pass
577+
578+            elif share.is_complete():
579                 # great! we already have it. easy.
580                 pass
581hunk ./src/allmydata/storage/server.py 353
582-            elif os.path.exists(incominghome):
583+            elif not share.is_complete():
584                 # Note that we don't create BucketWriters for shnums that
585                 # have a partial share (in incoming/), so if a second upload
586                 # occurs while the first is still in progress, the second
587hunk ./src/allmydata/storage/server.py 359
588                 # uploader will use different storage servers.
589                 pass
590-            elif (not limited) or (remaining_space >= max_space_per_bucket):
591-                # ok! we need to create the new share file.
592-                bw = BucketWriter(self, incominghome, finalhome,
593-                                  max_space_per_bucket, lease_info, canary)
594-                if self.no_storage:
595-                    bw.throw_out_all_data = True
596-                bucketwriters[shnum] = bw
597-                self._active_writers[bw] = 1
598-                if limited:
599-                    remaining_space -= max_space_per_bucket
600-            else:
601-                # bummer! not enough space to accept this bucket
602-                pass
603-
604-        if bucketwriters:
605-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
606 
607         self.add_latency("allocate", time.time() - start)
608         return alreadygot, bucketwriters
609hunk ./src/allmydata/storage/server.py 437
610             self.stats_provider.count('storage_server.bytes_added', consumed_size)
611         del self._active_writers[bw]
612 
613-    def _get_bucket_shares(self, storage_index):
614-        """Return a list of (shnum, pathname) tuples for files that hold
615-        shares for this storage_index. In each tuple, 'shnum' will always be
616-        the integer form of the last component of 'pathname'."""
617-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
618-        try:
619-            for f in os.listdir(storagedir):
620-                if NUM_RE.match(f):
621-                    filename = os.path.join(storagedir, f)
622-                    yield (int(f), filename)
623-        except OSError:
624-            # Commonly caused by there being no buckets at all.
625-            pass
626 
627     def remote_get_buckets(self, storage_index):
628         start = time.time()
629hunk ./src/allmydata/storage/server.py 444
630         si_s = si_b2a(storage_index)
631         log.msg("storage: get_buckets %s" % si_s)
632         bucketreaders = {} # k: sharenum, v: BucketReader
633-        for shnum, filename in self._get_bucket_shares(storage_index):
634+        for shnum, filename in self.backend.get_bucket_shares(storage_index):
635             bucketreaders[shnum] = BucketReader(self, filename,
636                                                 storage_index, shnum)
637         self.add_latency("get", time.time() - start)
638hunk ./src/allmydata/test/test_backends.py 10
639 import mock
640 
641 # This is the code that we're going to be testing.
642-from allmydata.storage.server import StorageServer
643+from allmydata.storage.server import StorageServer, FSBackend, NullBackend
644 
645 # The following share file contents was generated with
646 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
647hunk ./src/allmydata/test/test_backends.py 21
648 sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
649 
650 class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
651+    @mock.patch('time.time')
652+    @mock.patch('os.mkdir')
653+    @mock.patch('__builtin__.open')
654+    @mock.patch('os.listdir')
655+    @mock.patch('os.path.isdir')
656+    def test_create_server_null_backend(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
657+        """ This tests whether a server instance can be constructed
658+        with a null backend. The server instance fails the test if it
659+        tries to read or write to the file system. """
660+
661+        # Now begin the test.
662+        s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
663+
664+        self.failIf(mockisdir.called)
665+        self.failIf(mocklistdir.called)
666+        self.failIf(mockopen.called)
667+        self.failIf(mockmkdir.called)
668+
669+        # You passed!
670+
671+    @mock.patch('time.time')
672+    @mock.patch('os.mkdir')
673     @mock.patch('__builtin__.open')
674hunk ./src/allmydata/test/test_backends.py 44
675-    def test_create_server(self, mockopen):
676-        """ This tests whether a server instance can be constructed. """
677+    @mock.patch('os.listdir')
678+    @mock.patch('os.path.isdir')
679+    def test_create_server_fs_backend(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
680+        """ This tests whether a server instance can be constructed
681+        with a filesystem backend. To pass the test, it has to use the
682+        filesystem in only the prescribed ways. """
683 
684         def call_open(fname, mode):
685             if fname == 'testdir/bucket_counter.state':
686hunk ./src/allmydata/test/test_backends.py 58
687                 raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
688             elif fname == 'testdir/lease_checker.history':
689                 return StringIO()
690+            else:
691+                self.fail("Server with FS backend tried to open '%s' in mode '%s'" % (fname, mode))
692         mockopen.side_effect = call_open
693 
694         # Now begin the test.
695hunk ./src/allmydata/test/test_backends.py 63
696-        s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
697+        s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
698+
699+        self.failIf(mockisdir.called)
700+        self.failIf(mocklistdir.called)
701+        self.failIf(mockopen.called)
702+        self.failIf(mockmkdir.called)
703+        self.failIf(mocktime.called)
704 
705         # You passed!
706 
707hunk ./src/allmydata/test/test_backends.py 73
708-class TestServer(unittest.TestCase, ReallyEqualMixin):
709+class TestServerNullBackend(unittest.TestCase, ReallyEqualMixin):
710+    def setUp(self):
711+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
712+
713+    @mock.patch('os.mkdir')
714+    @mock.patch('__builtin__.open')
715+    @mock.patch('os.listdir')
716+    @mock.patch('os.path.isdir')
717+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
718+        """ Write a new share. """
719+
720+        # Now begin the test.
721+        alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
722+        bs[0].remote_write(0, 'a')
723+        self.failIf(mockisdir.called)
724+        self.failIf(mocklistdir.called)
725+        self.failIf(mockopen.called)
726+        self.failIf(mockmkdir.called)
727+
728+    @mock.patch('os.path.exists')
729+    @mock.patch('os.path.getsize')
730+    @mock.patch('__builtin__.open')
731+    @mock.patch('os.listdir')
732+    def test_read_share(self, mocklistdir, mockopen, mockgetsize, mockexists):
733+        """ This tests whether the code correctly finds and reads
734+        shares written out by old (Tahoe-LAFS <= v1.8.2)
735+        servers. There is a similar test in test_download, but that one
736+        is from the perspective of the client and exercises a deeper
737+        stack of code. This one is for exercising just the
738+        StorageServer object. """
739+
740+        # Now begin the test.
741+        bs = self.s.remote_get_buckets('teststorage_index')
742+
743+        self.failUnlessEqual(len(bs), 0)
744+        self.failIf(mocklistdir.called)
745+        self.failIf(mockopen.called)
746+        self.failIf(mockgetsize.called)
747+        self.failIf(mockexists.called)
748+
749+
750+class TestServerFSBackend(unittest.TestCase, ReallyEqualMixin):
751     @mock.patch('__builtin__.open')
752     def setUp(self, mockopen):
753         def call_open(fname, mode):
754hunk ./src/allmydata/test/test_backends.py 126
755                 return StringIO()
756         mockopen.side_effect = call_open
757 
758-        self.s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
759-
760+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
761 
762     @mock.patch('time.time')
763     @mock.patch('os.mkdir')
764hunk ./src/allmydata/test/test_backends.py 134
765     @mock.patch('os.listdir')
766     @mock.patch('os.path.isdir')
767     def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
768-        """Handle a report of corruption."""
769+        """ Write a new share. """
770 
771         def call_listdir(dirname):
772             self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
773hunk ./src/allmydata/test/test_backends.py 173
774         mockopen.side_effect = call_open
775         # Now begin the test.
776         alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
777-        print bs
778         bs[0].remote_write(0, 'a')
779         self.failUnlessReallyEqual(sharefile.buffer, share_file_data)
780 
781hunk ./src/allmydata/test/test_backends.py 176
782-
783     @mock.patch('os.path.exists')
784     @mock.patch('os.path.getsize')
785     @mock.patch('__builtin__.open')
786hunk ./src/allmydata/test/test_backends.py 218
787 
788         self.failUnlessEqual(len(bs), 1)
789         b = bs[0]
790+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
791         self.failUnlessReallyEqual(b.remote_read(0, datalen), share_data)
792         # If you try to read past the end you get the as much data as is there.
793         self.failUnlessReallyEqual(b.remote_read(0, datalen+20), share_data)
794hunk ./src/allmydata/test/test_backends.py 224
795         # If you start reading past the end of the file you get the empty string.
796         self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
797+
798+
799}
800[a temp patch used as a snapshot
801wilcoxjg@gmail.com**20110626052732
802 Ignore-this: 95f05e314eaec870afa04c76d979aa44
803] {
804hunk ./docs/configuration.rst 637
805   [storage]
806   enabled = True
807   readonly = True
808-  sizelimit = 10000000000
809 
810 
811   [helper]
812hunk ./docs/garbage-collection.rst 16
813 
814 When a file or directory in the virtual filesystem is no longer referenced,
815 the space that its shares occupied on each storage server can be freed,
816-making room for other shares. Tahoe currently uses a garbage collection
817+making room for other shares. Tahoe uses a garbage collection
818 ("GC") mechanism to implement this space-reclamation process. Each share has
819 one or more "leases", which are managed by clients who want the
820 file/directory to be retained. The storage server accepts each share for a
821hunk ./docs/garbage-collection.rst 34
822 the `<lease-tradeoffs.svg>`_ diagram to get an idea for the tradeoffs involved.
823 If lease renewal occurs quickly and with 100% reliability, than any renewal
824 time that is shorter than the lease duration will suffice, but a larger ratio
825-of duration-over-renewal-time will be more robust in the face of occasional
826+of lease duration to renewal time will be more robust in the face of occasional
827 delays or failures.
828 
829 The current recommended values for a small Tahoe grid are to renew the leases
830replace ./docs/garbage-collection.rst [A-Za-z_0-9\-\.] Tahoe Tahoe-LAFS
831hunk ./src/allmydata/client.py 260
832             sharetypes.append("mutable")
833         expiration_sharetypes = tuple(sharetypes)
834 
835+        if self.get_config("storage", "backend", "filesystem") == "filesystem":
836+            xyz
837+        xyz
838         ss = StorageServer(storedir, self.nodeid,
839                            reserved_space=reserved,
840                            discard_storage=discard,
841hunk ./src/allmydata/storage/crawler.py 234
842         f = open(tmpfile, "wb")
843         pickle.dump(self.state, f)
844         f.close()
845-        fileutil.move_into_place(tmpfile, self.statefile)
846+        fileutil.move_into_place(tmpfile, self.statefname)
847 
848     def startService(self):
849         # arrange things to look like we were just sleeping, so
850}
851[snapshot of progress on backend implementation (not suitable for trunk)
852wilcoxjg@gmail.com**20110626053244
853 Ignore-this: 50c764af791c2b99ada8289546806a0a
854] {
855adddir ./src/allmydata/storage/backends
856adddir ./src/allmydata/storage/backends/das
857move ./src/allmydata/storage/expirer.py ./src/allmydata/storage/backends/das/expirer.py
858adddir ./src/allmydata/storage/backends/null
859hunk ./src/allmydata/interfaces.py 270
860         store that on disk.
861         """
862 
863+class IStorageBackend(Interface):
864+    """
865+    Objects of this kind live on the server side and are used by the
866+    storage server object.
867+    """
868+    def get_available_space(self, reserved_space):
869+        """ Returns available space for share storage in bytes, or
870+        None if this information is not available or if the available
871+        space is unlimited.
872+
873+        If the backend is configured for read-only mode then this will
874+        return 0.
875+
876+        reserved_space is how many bytes to subtract from the answer, so
877+        you can pass how many bytes you would like to leave unused on this
878+        filesystem as reserved_space. """
879+
880+    def get_bucket_shares(self):
881+        """XXX"""
882+
883+    def get_share(self):
884+        """XXX"""
885+
886+    def make_bucket_writer(self):
887+        """XXX"""
888+
889+class IStorageBackendShare(Interface):
890+    """
891+    This object contains as much as all of the share data.  It is intended
892+    for lazy evaluation such that in many use cases substantially less than
893+    all of the share data will be accessed.
894+    """
895+    def is_complete(self):
896+        """
897+        Returns the share state, or None if the share does not exist.
898+        """
899+
900 class IStorageBucketWriter(Interface):
901     """
902     Objects of this kind live on the client side.
903hunk ./src/allmydata/interfaces.py 2492
904 
905 class EmptyPathnameComponentError(Exception):
906     """The webapi disallows empty pathname components."""
907+
908+class IShareStore(Interface):
909+    pass
910+
911addfile ./src/allmydata/storage/backends/__init__.py
912addfile ./src/allmydata/storage/backends/das/__init__.py
913addfile ./src/allmydata/storage/backends/das/core.py
914hunk ./src/allmydata/storage/backends/das/core.py 1
915+from allmydata.interfaces import IStorageBackend
916+from allmydata.storage.backends.base import Backend
917+from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
918+from allmydata.util.assertutil import precondition
919+
920+import os, re, weakref, struct, time
921+
922+from foolscap.api import Referenceable
923+from twisted.application import service
924+
925+from zope.interface import implements
926+from allmydata.interfaces import RIStorageServer, IStatsProducer, IShareStore
927+from allmydata.util import fileutil, idlib, log, time_format
928+import allmydata # for __full_version__
929+
930+from allmydata.storage.common import si_b2a, si_a2b, storage_index_to_dir
931+_pyflakes_hush = [si_b2a, si_a2b, storage_index_to_dir] # re-exported
932+from allmydata.storage.lease import LeaseInfo
933+from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
934+     create_mutable_sharefile
935+from allmydata.storage.backends.das.immutable import NullBucketWriter, BucketWriter, BucketReader
936+from allmydata.storage.crawler import FSBucketCountingCrawler
937+from allmydata.storage.backends.das.expirer import FSLeaseCheckingCrawler
938+
939+from zope.interface import implements
940+
941+class DASCore(Backend):
942+    implements(IStorageBackend)
943+    def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
944+        Backend.__init__(self)
945+
946+        self._setup_storage(storedir, readonly, reserved_space)
947+        self._setup_corruption_advisory()
948+        self._setup_bucket_counter()
949+        self._setup_lease_checkerf(expiration_policy)
950+
951+    def _setup_storage(self, storedir, readonly, reserved_space):
952+        self.storedir = storedir
953+        self.readonly = readonly
954+        self.reserved_space = int(reserved_space)
955+        if self.reserved_space:
956+            if self.get_available_space() is None:
957+                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",
958+                        umid="0wZ27w", level=log.UNUSUAL)
959+
960+        self.sharedir = os.path.join(self.storedir, "shares")
961+        fileutil.make_dirs(self.sharedir)
962+        self.incomingdir = os.path.join(self.sharedir, 'incoming')
963+        self._clean_incomplete()
964+
965+    def _clean_incomplete(self):
966+        fileutil.rm_dir(self.incomingdir)
967+        fileutil.make_dirs(self.incomingdir)
968+
969+    def _setup_corruption_advisory(self):
970+        # we don't actually create the corruption-advisory dir until necessary
971+        self.corruption_advisory_dir = os.path.join(self.storedir,
972+                                                    "corruption-advisories")
973+
974+    def _setup_bucket_counter(self):
975+        statefname = os.path.join(self.storedir, "bucket_counter.state")
976+        self.bucket_counter = FSBucketCountingCrawler(statefname)
977+        self.bucket_counter.setServiceParent(self)
978+
979+    def _setup_lease_checkerf(self, expiration_policy):
980+        statefile = os.path.join(self.storedir, "lease_checker.state")
981+        historyfile = os.path.join(self.storedir, "lease_checker.history")
982+        self.lease_checker = FSLeaseCheckingCrawler(statefile, historyfile, expiration_policy)
983+        self.lease_checker.setServiceParent(self)
984+
985+    def get_available_space(self):
986+        if self.readonly:
987+            return 0
988+        return fileutil.get_available_space(self.storedir, self.reserved_space)
989+
990+    def get_shares(self, storage_index):
991+        """Return a list of the FSBShare objects that correspond to the passed storage_index."""
992+        finalstoragedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
993+        try:
994+            for f in os.listdir(finalstoragedir):
995+                if NUM_RE.match(f):
996+                    filename = os.path.join(finalstoragedir, f)
997+                    yield FSBShare(filename, int(f))
998+        except OSError:
999+            # Commonly caused by there being no buckets at all.
1000+            pass
1001+       
1002+    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
1003+        immsh = ImmutableShare(self.sharedir, storage_index, shnum, max_size=max_space_per_bucket, create=True)
1004+        bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1005+        return bw
1006+       
1007+
1008+# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1009+# and share data. The share data is accessed by RIBucketWriter.write and
1010+# RIBucketReader.read . The lease information is not accessible through these
1011+# interfaces.
1012+
1013+# The share file has the following layout:
1014+#  0x00: share file version number, four bytes, current version is 1
1015+#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1016+#  0x08: number of leases, four bytes big-endian
1017+#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1018+#  A+0x0c = B: first lease. Lease format is:
1019+#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1020+#   B+0x04: renew secret, 32 bytes (SHA256)
1021+#   B+0x24: cancel secret, 32 bytes (SHA256)
1022+#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1023+#   B+0x48: next lease, or end of record
1024+
1025+# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1026+# but it is still filled in by storage servers in case the storage server
1027+# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1028+# share file is moved from one storage server to another. The value stored in
1029+# this field is truncated, so if the actual share data length is >= 2**32,
1030+# then the value stored in this field will be the actual share data length
1031+# modulo 2**32.
1032+
1033+class ImmutableShare:
1034+    LEASE_SIZE = struct.calcsize(">L32s32sL")
1035+    sharetype = "immutable"
1036+
1037+    def __init__(self, sharedir, storageindex, shnum, max_size=None, create=False):
1038+        """ If max_size is not None then I won't allow more than
1039+        max_size to be written to me. If create=True then max_size
1040+        must not be None. """
1041+        precondition((max_size is not None) or (not create), max_size, create)
1042+        self.shnum = shnum
1043+        self.fname = os.path.join(sharedir, storage_index_to_dir(storageindex), str(shnum))
1044+        self._max_size = max_size
1045+        if create:
1046+            # touch the file, so later callers will see that we're working on
1047+            # it. Also construct the metadata.
1048+            assert not os.path.exists(self.fname)
1049+            fileutil.make_dirs(os.path.dirname(self.fname))
1050+            f = open(self.fname, 'wb')
1051+            # The second field -- the four-byte share data length -- is no
1052+            # longer used as of Tahoe v1.3.0, but we continue to write it in
1053+            # there in case someone downgrades a storage server from >=
1054+            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1055+            # server to another, etc. We do saturation -- a share data length
1056+            # larger than 2**32-1 (what can fit into the field) is marked as
1057+            # the largest length that can fit into the field. That way, even
1058+            # if this does happen, the old < v1.3.0 server will still allow
1059+            # clients to read the first part of the share.
1060+            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1061+            f.close()
1062+            self._lease_offset = max_size + 0x0c
1063+            self._num_leases = 0
1064+        else:
1065+            f = open(self.fname, 'rb')
1066+            filesize = os.path.getsize(self.fname)
1067+            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1068+            f.close()
1069+            if version != 1:
1070+                msg = "sharefile %s had version %d but we wanted 1" % \
1071+                      (self.fname, version)
1072+                raise UnknownImmutableContainerVersionError(msg)
1073+            self._num_leases = num_leases
1074+            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1075+        self._data_offset = 0xc
1076+
1077+    def unlink(self):
1078+        os.unlink(self.fname)
1079+
1080+    def read_share_data(self, offset, length):
1081+        precondition(offset >= 0)
1082+        # Reads beyond the end of the data are truncated. Reads that start
1083+        # beyond the end of the data return an empty string.
1084+        seekpos = self._data_offset+offset
1085+        fsize = os.path.getsize(self.fname)
1086+        actuallength = max(0, min(length, fsize-seekpos))
1087+        if actuallength == 0:
1088+            return ""
1089+        f = open(self.fname, 'rb')
1090+        f.seek(seekpos)
1091+        return f.read(actuallength)
1092+
1093+    def write_share_data(self, offset, data):
1094+        length = len(data)
1095+        precondition(offset >= 0, offset)
1096+        if self._max_size is not None and offset+length > self._max_size:
1097+            raise DataTooLargeError(self._max_size, offset, length)
1098+        f = open(self.fname, 'rb+')
1099+        real_offset = self._data_offset+offset
1100+        f.seek(real_offset)
1101+        assert f.tell() == real_offset
1102+        f.write(data)
1103+        f.close()
1104+
1105+    def _write_lease_record(self, f, lease_number, lease_info):
1106+        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1107+        f.seek(offset)
1108+        assert f.tell() == offset
1109+        f.write(lease_info.to_immutable_data())
1110+
1111+    def _read_num_leases(self, f):
1112+        f.seek(0x08)
1113+        (num_leases,) = struct.unpack(">L", f.read(4))
1114+        return num_leases
1115+
1116+    def _write_num_leases(self, f, num_leases):
1117+        f.seek(0x08)
1118+        f.write(struct.pack(">L", num_leases))
1119+
1120+    def _truncate_leases(self, f, num_leases):
1121+        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1122+
1123+    def get_leases(self):
1124+        """Yields a LeaseInfo instance for all leases."""
1125+        f = open(self.fname, 'rb')
1126+        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1127+        f.seek(self._lease_offset)
1128+        for i in range(num_leases):
1129+            data = f.read(self.LEASE_SIZE)
1130+            if data:
1131+                yield LeaseInfo().from_immutable_data(data)
1132+
1133+    def add_lease(self, lease_info):
1134+        f = open(self.fname, 'rb+')
1135+        num_leases = self._read_num_leases(f)
1136+        self._write_lease_record(f, num_leases, lease_info)
1137+        self._write_num_leases(f, num_leases+1)
1138+        f.close()
1139+
1140+    def renew_lease(self, renew_secret, new_expire_time):
1141+        for i,lease in enumerate(self.get_leases()):
1142+            if constant_time_compare(lease.renew_secret, renew_secret):
1143+                # yup. See if we need to update the owner time.
1144+                if new_expire_time > lease.expiration_time:
1145+                    # yes
1146+                    lease.expiration_time = new_expire_time
1147+                    f = open(self.fname, 'rb+')
1148+                    self._write_lease_record(f, i, lease)
1149+                    f.close()
1150+                return
1151+        raise IndexError("unable to renew non-existent lease")
1152+
1153+    def add_or_renew_lease(self, lease_info):
1154+        try:
1155+            self.renew_lease(lease_info.renew_secret,
1156+                             lease_info.expiration_time)
1157+        except IndexError:
1158+            self.add_lease(lease_info)
1159+
1160+
1161+    def cancel_lease(self, cancel_secret):
1162+        """Remove a lease with the given cancel_secret. If the last lease is
1163+        cancelled, the file will be removed. Return the number of bytes that
1164+        were freed (by truncating the list of leases, and possibly by
1165+        deleting the file. Raise IndexError if there was no lease with the
1166+        given cancel_secret.
1167+        """
1168+
1169+        leases = list(self.get_leases())
1170+        num_leases_removed = 0
1171+        for i,lease in enumerate(leases):
1172+            if constant_time_compare(lease.cancel_secret, cancel_secret):
1173+                leases[i] = None
1174+                num_leases_removed += 1
1175+        if not num_leases_removed:
1176+            raise IndexError("unable to find matching lease to cancel")
1177+        if num_leases_removed:
1178+            # pack and write out the remaining leases. We write these out in
1179+            # the same order as they were added, so that if we crash while
1180+            # doing this, we won't lose any non-cancelled leases.
1181+            leases = [l for l in leases if l] # remove the cancelled leases
1182+            f = open(self.fname, 'rb+')
1183+            for i,lease in enumerate(leases):
1184+                self._write_lease_record(f, i, lease)
1185+            self._write_num_leases(f, len(leases))
1186+            self._truncate_leases(f, len(leases))
1187+            f.close()
1188+        space_freed = self.LEASE_SIZE * num_leases_removed
1189+        if not len(leases):
1190+            space_freed += os.stat(self.fname)[stat.ST_SIZE]
1191+            self.unlink()
1192+        return space_freed
1193hunk ./src/allmydata/storage/backends/das/expirer.py 2
1194 import time, os, pickle, struct
1195-from allmydata.storage.crawler import ShareCrawler
1196-from allmydata.storage.shares import get_share_file
1197+from allmydata.storage.crawler import FSShareCrawler
1198 from allmydata.storage.common import UnknownMutableContainerVersionError, \
1199      UnknownImmutableContainerVersionError
1200 from twisted.python import log as twlog
1201hunk ./src/allmydata/storage/backends/das/expirer.py 7
1202 
1203-class LeaseCheckingCrawler(ShareCrawler):
1204+class FSLeaseCheckingCrawler(FSShareCrawler):
1205     """I examine the leases on all shares, determining which are still valid
1206     and which have expired. I can remove the expired leases (if so
1207     configured), and the share will be deleted when the last lease is
1208hunk ./src/allmydata/storage/backends/das/expirer.py 50
1209     slow_start = 360 # wait 6 minutes after startup
1210     minimum_cycle_time = 12*60*60 # not more than twice per day
1211 
1212-    def __init__(self, statefile, historyfile,
1213-                 expiration_enabled, mode,
1214-                 override_lease_duration, # used if expiration_mode=="age"
1215-                 cutoff_date, # used if expiration_mode=="cutoff-date"
1216-                 sharetypes):
1217+    def __init__(self, statefile, historyfile, expiration_policy):
1218         self.historyfile = historyfile
1219hunk ./src/allmydata/storage/backends/das/expirer.py 52
1220-        self.expiration_enabled = expiration_enabled
1221-        self.mode = mode
1222+        self.expiration_enabled = expiration_policy['enabled']
1223+        self.mode = expiration_policy['mode']
1224         self.override_lease_duration = None
1225         self.cutoff_date = None
1226         if self.mode == "age":
1227hunk ./src/allmydata/storage/backends/das/expirer.py 57
1228-            assert isinstance(override_lease_duration, (int, type(None)))
1229-            self.override_lease_duration = override_lease_duration # seconds
1230+            assert isinstance(expiration_policy['override_lease_duration'], (int, type(None)))
1231+            self.override_lease_duration = expiration_policy['override_lease_duration']# seconds
1232         elif self.mode == "cutoff-date":
1233hunk ./src/allmydata/storage/backends/das/expirer.py 60
1234-            assert isinstance(cutoff_date, int) # seconds-since-epoch
1235+            assert isinstance(expiration_policy['cutoff_date'], int) # seconds-since-epoch
1236             assert cutoff_date is not None
1237hunk ./src/allmydata/storage/backends/das/expirer.py 62
1238-            self.cutoff_date = cutoff_date
1239+            self.cutoff_date = expiration_policy['cutoff_date']
1240         else:
1241hunk ./src/allmydata/storage/backends/das/expirer.py 64
1242-            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
1243-        self.sharetypes_to_expire = sharetypes
1244-        ShareCrawler.__init__(self, statefile)
1245+            raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % expiration_policy['mode'])
1246+        self.sharetypes_to_expire = expiration_policy['sharetypes']
1247+        FSShareCrawler.__init__(self, statefile)
1248 
1249     def add_initial_state(self):
1250         # we fill ["cycle-to-date"] here (even though they will be reset in
1251hunk ./src/allmydata/storage/backends/das/expirer.py 156
1252 
1253     def process_share(self, sharefilename):
1254         # first, find out what kind of a share it is
1255-        sf = get_share_file(sharefilename)
1256+        f = open(sharefilename, "rb")
1257+        prefix = f.read(32)
1258+        f.close()
1259+        if prefix == MutableShareFile.MAGIC:
1260+            sf = MutableShareFile(sharefilename)
1261+        else:
1262+            # otherwise assume it's immutable
1263+            sf = FSBShare(sharefilename)
1264         sharetype = sf.sharetype
1265         now = time.time()
1266         s = self.stat(sharefilename)
1267addfile ./src/allmydata/storage/backends/null/__init__.py
1268addfile ./src/allmydata/storage/backends/null/core.py
1269hunk ./src/allmydata/storage/backends/null/core.py 1
1270+from allmydata.storage.backends.base import Backend
1271+
1272+class NullCore(Backend):
1273+    def __init__(self):
1274+        Backend.__init__(self)
1275+
1276+    def get_available_space(self):
1277+        return None
1278+
1279+    def get_shares(self, storage_index):
1280+        return set()
1281+
1282+    def get_share(self, storage_index, sharenum):
1283+        return None
1284+
1285+    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
1286+        return NullBucketWriter()
1287hunk ./src/allmydata/storage/crawler.py 12
1288 class TimeSliceExceeded(Exception):
1289     pass
1290 
1291-class ShareCrawler(service.MultiService):
1292+class FSShareCrawler(service.MultiService):
1293     """A subcless of ShareCrawler is attached to a StorageServer, and
1294     periodically walks all of its shares, processing each one in some
1295     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
1296hunk ./src/allmydata/storage/crawler.py 68
1297     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
1298     minimum_cycle_time = 300 # don't run a cycle faster than this
1299 
1300-    def __init__(self, backend, statefile, allowed_cpu_percentage=None):
1301+    def __init__(self, statefname, allowed_cpu_percentage=None):
1302         service.MultiService.__init__(self)
1303         if allowed_cpu_percentage is not None:
1304             self.allowed_cpu_percentage = allowed_cpu_percentage
1305hunk ./src/allmydata/storage/crawler.py 72
1306-        self.backend = backend
1307+        self.statefname = statefname
1308         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
1309                          for i in range(2**10)]
1310         self.prefixes.sort()
1311hunk ./src/allmydata/storage/crawler.py 192
1312         #                            of the last bucket to be processed, or
1313         #                            None if we are sleeping between cycles
1314         try:
1315-            f = open(self.statefile, "rb")
1316+            f = open(self.statefname, "rb")
1317             state = pickle.load(f)
1318             f.close()
1319         except EnvironmentError:
1320hunk ./src/allmydata/storage/crawler.py 230
1321         else:
1322             last_complete_prefix = self.prefixes[lcpi]
1323         self.state["last-complete-prefix"] = last_complete_prefix
1324-        tmpfile = self.statefile + ".tmp"
1325+        tmpfile = self.statefname + ".tmp"
1326         f = open(tmpfile, "wb")
1327         pickle.dump(self.state, f)
1328         f.close()
1329hunk ./src/allmydata/storage/crawler.py 433
1330         pass
1331 
1332 
1333-class BucketCountingCrawler(ShareCrawler):
1334+class FSBucketCountingCrawler(FSShareCrawler):
1335     """I keep track of how many buckets are being managed by this server.
1336     This is equivalent to the number of distributed files and directories for
1337     which I am providing storage. The actual number of files+directories in
1338hunk ./src/allmydata/storage/crawler.py 446
1339 
1340     minimum_cycle_time = 60*60 # we don't need this more than once an hour
1341 
1342-    def __init__(self, statefile, num_sample_prefixes=1):
1343-        ShareCrawler.__init__(self, statefile)
1344+    def __init__(self, statefname, num_sample_prefixes=1):
1345+        FSShareCrawler.__init__(self, statefname)
1346         self.num_sample_prefixes = num_sample_prefixes
1347 
1348     def add_initial_state(self):
1349hunk ./src/allmydata/storage/immutable.py 14
1350 from allmydata.storage.common import UnknownImmutableContainerVersionError, \
1351      DataTooLargeError
1352 
1353-# each share file (in storage/shares/$SI/$SHNUM) contains lease information
1354-# and share data. The share data is accessed by RIBucketWriter.write and
1355-# RIBucketReader.read . The lease information is not accessible through these
1356-# interfaces.
1357-
1358-# The share file has the following layout:
1359-#  0x00: share file version number, four bytes, current version is 1
1360-#  0x04: share data length, four bytes big-endian = A # See Footnote 1 below.
1361-#  0x08: number of leases, four bytes big-endian
1362-#  0x0c: beginning of share data (see immutable.layout.WriteBucketProxy)
1363-#  A+0x0c = B: first lease. Lease format is:
1364-#   B+0x00: owner number, 4 bytes big-endian, 0 is reserved for no-owner
1365-#   B+0x04: renew secret, 32 bytes (SHA256)
1366-#   B+0x24: cancel secret, 32 bytes (SHA256)
1367-#   B+0x44: expiration time, 4 bytes big-endian seconds-since-epoch
1368-#   B+0x48: next lease, or end of record
1369-
1370-# Footnote 1: as of Tahoe v1.3.0 this field is not used by storage servers,
1371-# but it is still filled in by storage servers in case the storage server
1372-# software gets downgraded from >= Tahoe v1.3.0 to < Tahoe v1.3.0, or the
1373-# share file is moved from one storage server to another. The value stored in
1374-# this field is truncated, so if the actual share data length is >= 2**32,
1375-# then the value stored in this field will be the actual share data length
1376-# modulo 2**32.
1377-
1378-class ShareFile:
1379-    LEASE_SIZE = struct.calcsize(">L32s32sL")
1380-    sharetype = "immutable"
1381-
1382-    def __init__(self, filename, max_size=None, create=False):
1383-        """ If max_size is not None then I won't allow more than
1384-        max_size to be written to me. If create=True then max_size
1385-        must not be None. """
1386-        precondition((max_size is not None) or (not create), max_size, create)
1387-        self.home = filename
1388-        self._max_size = max_size
1389-        if create:
1390-            # touch the file, so later callers will see that we're working on
1391-            # it. Also construct the metadata.
1392-            assert not os.path.exists(self.home)
1393-            fileutil.make_dirs(os.path.dirname(self.home))
1394-            f = open(self.home, 'wb')
1395-            # The second field -- the four-byte share data length -- is no
1396-            # longer used as of Tahoe v1.3.0, but we continue to write it in
1397-            # there in case someone downgrades a storage server from >=
1398-            # Tahoe-1.3.0 to < Tahoe-1.3.0, or moves a share file from one
1399-            # server to another, etc. We do saturation -- a share data length
1400-            # larger than 2**32-1 (what can fit into the field) is marked as
1401-            # the largest length that can fit into the field. That way, even
1402-            # if this does happen, the old < v1.3.0 server will still allow
1403-            # clients to read the first part of the share.
1404-            f.write(struct.pack(">LLL", 1, min(2**32-1, max_size), 0))
1405-            f.close()
1406-            self._lease_offset = max_size + 0x0c
1407-            self._num_leases = 0
1408-        else:
1409-            f = open(self.home, 'rb')
1410-            filesize = os.path.getsize(self.home)
1411-            (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1412-            f.close()
1413-            if version != 1:
1414-                msg = "sharefile %s had version %d but we wanted 1" % \
1415-                      (filename, version)
1416-                raise UnknownImmutableContainerVersionError(msg)
1417-            self._num_leases = num_leases
1418-            self._lease_offset = filesize - (num_leases * self.LEASE_SIZE)
1419-        self._data_offset = 0xc
1420-
1421-    def unlink(self):
1422-        os.unlink(self.home)
1423-
1424-    def read_share_data(self, offset, length):
1425-        precondition(offset >= 0)
1426-        # Reads beyond the end of the data are truncated. Reads that start
1427-        # beyond the end of the data return an empty string.
1428-        seekpos = self._data_offset+offset
1429-        fsize = os.path.getsize(self.home)
1430-        actuallength = max(0, min(length, fsize-seekpos))
1431-        if actuallength == 0:
1432-            return ""
1433-        f = open(self.home, 'rb')
1434-        f.seek(seekpos)
1435-        return f.read(actuallength)
1436-
1437-    def write_share_data(self, offset, data):
1438-        length = len(data)
1439-        precondition(offset >= 0, offset)
1440-        if self._max_size is not None and offset+length > self._max_size:
1441-            raise DataTooLargeError(self._max_size, offset, length)
1442-        f = open(self.home, 'rb+')
1443-        real_offset = self._data_offset+offset
1444-        f.seek(real_offset)
1445-        assert f.tell() == real_offset
1446-        f.write(data)
1447-        f.close()
1448-
1449-    def _write_lease_record(self, f, lease_number, lease_info):
1450-        offset = self._lease_offset + lease_number * self.LEASE_SIZE
1451-        f.seek(offset)
1452-        assert f.tell() == offset
1453-        f.write(lease_info.to_immutable_data())
1454-
1455-    def _read_num_leases(self, f):
1456-        f.seek(0x08)
1457-        (num_leases,) = struct.unpack(">L", f.read(4))
1458-        return num_leases
1459-
1460-    def _write_num_leases(self, f, num_leases):
1461-        f.seek(0x08)
1462-        f.write(struct.pack(">L", num_leases))
1463-
1464-    def _truncate_leases(self, f, num_leases):
1465-        f.truncate(self._lease_offset + num_leases * self.LEASE_SIZE)
1466-
1467-    def get_leases(self):
1468-        """Yields a LeaseInfo instance for all leases."""
1469-        f = open(self.home, 'rb')
1470-        (version, unused, num_leases) = struct.unpack(">LLL", f.read(0xc))
1471-        f.seek(self._lease_offset)
1472-        for i in range(num_leases):
1473-            data = f.read(self.LEASE_SIZE)
1474-            if data:
1475-                yield LeaseInfo().from_immutable_data(data)
1476-
1477-    def add_lease(self, lease_info):
1478-        f = open(self.home, 'rb+')
1479-        num_leases = self._read_num_leases(f)
1480-        self._write_lease_record(f, num_leases, lease_info)
1481-        self._write_num_leases(f, num_leases+1)
1482-        f.close()
1483-
1484-    def renew_lease(self, renew_secret, new_expire_time):
1485-        for i,lease in enumerate(self.get_leases()):
1486-            if constant_time_compare(lease.renew_secret, renew_secret):
1487-                # yup. See if we need to update the owner time.
1488-                if new_expire_time > lease.expiration_time:
1489-                    # yes
1490-                    lease.expiration_time = new_expire_time
1491-                    f = open(self.home, 'rb+')
1492-                    self._write_lease_record(f, i, lease)
1493-                    f.close()
1494-                return
1495-        raise IndexError("unable to renew non-existent lease")
1496-
1497-    def add_or_renew_lease(self, lease_info):
1498-        try:
1499-            self.renew_lease(lease_info.renew_secret,
1500-                             lease_info.expiration_time)
1501-        except IndexError:
1502-            self.add_lease(lease_info)
1503-
1504-
1505-    def cancel_lease(self, cancel_secret):
1506-        """Remove a lease with the given cancel_secret. If the last lease is
1507-        cancelled, the file will be removed. Return the number of bytes that
1508-        were freed (by truncating the list of leases, and possibly by
1509-        deleting the file. Raise IndexError if there was no lease with the
1510-        given cancel_secret.
1511-        """
1512-
1513-        leases = list(self.get_leases())
1514-        num_leases_removed = 0
1515-        for i,lease in enumerate(leases):
1516-            if constant_time_compare(lease.cancel_secret, cancel_secret):
1517-                leases[i] = None
1518-                num_leases_removed += 1
1519-        if not num_leases_removed:
1520-            raise IndexError("unable to find matching lease to cancel")
1521-        if num_leases_removed:
1522-            # pack and write out the remaining leases. We write these out in
1523-            # the same order as they were added, so that if we crash while
1524-            # doing this, we won't lose any non-cancelled leases.
1525-            leases = [l for l in leases if l] # remove the cancelled leases
1526-            f = open(self.home, 'rb+')
1527-            for i,lease in enumerate(leases):
1528-                self._write_lease_record(f, i, lease)
1529-            self._write_num_leases(f, len(leases))
1530-            self._truncate_leases(f, len(leases))
1531-            f.close()
1532-        space_freed = self.LEASE_SIZE * num_leases_removed
1533-        if not len(leases):
1534-            space_freed += os.stat(self.home)[stat.ST_SIZE]
1535-            self.unlink()
1536-        return space_freed
1537-class NullBucketWriter(Referenceable):
1538-    implements(RIBucketWriter)
1539-
1540-    def remote_write(self, offset, data):
1541-        return
1542-
1543 class BucketWriter(Referenceable):
1544     implements(RIBucketWriter)
1545 
1546hunk ./src/allmydata/storage/immutable.py 17
1547-    def __init__(self, ss, incominghome, finalhome, max_size, lease_info, canary):
1548+    def __init__(self, ss, immutableshare, max_size, lease_info, canary):
1549         self.ss = ss
1550hunk ./src/allmydata/storage/immutable.py 19
1551-        self.incominghome = incominghome
1552-        self.finalhome = finalhome
1553         self._max_size = max_size # don't allow the client to write more than this
1554         self._canary = canary
1555         self._disconnect_marker = canary.notifyOnDisconnect(self._disconnected)
1556hunk ./src/allmydata/storage/immutable.py 24
1557         self.closed = False
1558         self.throw_out_all_data = False
1559-        self._sharefile = ShareFile(incominghome, create=True, max_size=max_size)
1560+        self._sharefile = immutableshare
1561         # also, add our lease to the file now, so that other ones can be
1562         # added by simultaneous uploaders
1563         self._sharefile.add_lease(lease_info)
1564hunk ./src/allmydata/storage/server.py 16
1565 from allmydata.storage.lease import LeaseInfo
1566 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
1567      create_mutable_sharefile
1568-from allmydata.storage.immutable import ShareFile, NullBucketWriter, BucketWriter, BucketReader
1569-from allmydata.storage.crawler import BucketCountingCrawler
1570-from allmydata.storage.expirer import LeaseCheckingCrawler
1571 
1572 from zope.interface import implements
1573 
1574hunk ./src/allmydata/storage/server.py 19
1575-# A Backend is a MultiService so that its server's crawlers (if the server has any) can
1576-# be started and stopped.
1577-class Backend(service.MultiService):
1578-    implements(IStatsProducer)
1579-    def __init__(self):
1580-        service.MultiService.__init__(self)
1581-
1582-    def get_bucket_shares(self):
1583-        """XXX"""
1584-        raise NotImplementedError
1585-
1586-    def get_share(self):
1587-        """XXX"""
1588-        raise NotImplementedError
1589-
1590-    def make_bucket_writer(self):
1591-        """XXX"""
1592-        raise NotImplementedError
1593-
1594-class NullBackend(Backend):
1595-    def __init__(self):
1596-        Backend.__init__(self)
1597-
1598-    def get_available_space(self):
1599-        return None
1600-
1601-    def get_bucket_shares(self, storage_index):
1602-        return set()
1603-
1604-    def get_share(self, storage_index, sharenum):
1605-        return None
1606-
1607-    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
1608-        return NullBucketWriter()
1609-
1610-class FSBackend(Backend):
1611-    def __init__(self, storedir, readonly=False, reserved_space=0):
1612-        Backend.__init__(self)
1613-
1614-        self._setup_storage(storedir, readonly, reserved_space)
1615-        self._setup_corruption_advisory()
1616-        self._setup_bucket_counter()
1617-        self._setup_lease_checkerf()
1618-
1619-    def _setup_storage(self, storedir, readonly, reserved_space):
1620-        self.storedir = storedir
1621-        self.readonly = readonly
1622-        self.reserved_space = int(reserved_space)
1623-        if self.reserved_space:
1624-            if self.get_available_space() is None:
1625-                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",
1626-                        umid="0wZ27w", level=log.UNUSUAL)
1627-
1628-        self.sharedir = os.path.join(self.storedir, "shares")
1629-        fileutil.make_dirs(self.sharedir)
1630-        self.incomingdir = os.path.join(self.sharedir, 'incoming')
1631-        self._clean_incomplete()
1632-
1633-    def _clean_incomplete(self):
1634-        fileutil.rm_dir(self.incomingdir)
1635-        fileutil.make_dirs(self.incomingdir)
1636-
1637-    def _setup_corruption_advisory(self):
1638-        # we don't actually create the corruption-advisory dir until necessary
1639-        self.corruption_advisory_dir = os.path.join(self.storedir,
1640-                                                    "corruption-advisories")
1641-
1642-    def _setup_bucket_counter(self):
1643-        statefile = os.path.join(self.storedir, "bucket_counter.state")
1644-        self.bucket_counter = BucketCountingCrawler(statefile)
1645-        self.bucket_counter.setServiceParent(self)
1646-
1647-    def _setup_lease_checkerf(self):
1648-        statefile = os.path.join(self.storedir, "lease_checker.state")
1649-        historyfile = os.path.join(self.storedir, "lease_checker.history")
1650-        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile,
1651-                                   expiration_enabled, expiration_mode,
1652-                                   expiration_override_lease_duration,
1653-                                   expiration_cutoff_date,
1654-                                   expiration_sharetypes)
1655-        self.lease_checker.setServiceParent(self)
1656-
1657-    def get_available_space(self):
1658-        if self.readonly:
1659-            return 0
1660-        return fileutil.get_available_space(self.storedir, self.reserved_space)
1661-
1662-    def get_bucket_shares(self, storage_index):
1663-        """Return a list of (shnum, pathname) tuples for files that hold
1664-        shares for this storage_index. In each tuple, 'shnum' will always be
1665-        the integer form of the last component of 'pathname'."""
1666-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
1667-        try:
1668-            for f in os.listdir(storagedir):
1669-                if NUM_RE.match(f):
1670-                    filename = os.path.join(storagedir, f)
1671-                    yield (int(f), filename)
1672-        except OSError:
1673-            # Commonly caused by there being no buckets at all.
1674-            pass
1675-
1676 # storage/
1677 # storage/shares/incoming
1678 #   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
1679hunk ./src/allmydata/storage/server.py 32
1680 # $SHARENUM matches this regex:
1681 NUM_RE=re.compile("^[0-9]+$")
1682 
1683-
1684-
1685 class StorageServer(service.MultiService, Referenceable):
1686     implements(RIStorageServer, IStatsProducer)
1687     name = 'storage'
1688hunk ./src/allmydata/storage/server.py 35
1689-    LeaseCheckerClass = LeaseCheckingCrawler
1690 
1691     def __init__(self, nodeid, backend, reserved_space=0,
1692                  readonly_storage=False,
1693hunk ./src/allmydata/storage/server.py 38
1694-                 stats_provider=None,
1695-                 expiration_enabled=False,
1696-                 expiration_mode="age",
1697-                 expiration_override_lease_duration=None,
1698-                 expiration_cutoff_date=None,
1699-                 expiration_sharetypes=("mutable", "immutable")):
1700+                 stats_provider=None ):
1701         service.MultiService.__init__(self)
1702         assert isinstance(nodeid, str)
1703         assert len(nodeid) == 20
1704hunk ./src/allmydata/storage/server.py 217
1705         # they asked about: this will save them a lot of work. Add or update
1706         # leases for all of them: if they want us to hold shares for this
1707         # file, they'll want us to hold leases for this file.
1708-        for (shnum, fn) in self.backend.get_bucket_shares(storage_index):
1709-            alreadygot.add(shnum)
1710-            sf = ShareFile(fn)
1711-            sf.add_or_renew_lease(lease_info)
1712-
1713-        for shnum in sharenums:
1714-            share = self.backend.get_share(storage_index, shnum)
1715+        for share in self.backend.get_shares(storage_index):
1716+            alreadygot.add(share.shnum)
1717+            share.add_or_renew_lease(lease_info)
1718 
1719hunk ./src/allmydata/storage/server.py 221
1720-            if not share:
1721-                if (not limited) or (remaining_space >= max_space_per_bucket):
1722-                    # ok! we need to create the new share file.
1723-                    bw = self.backend.make_bucket_writer(storage_index, shnum,
1724-                                      max_space_per_bucket, lease_info, canary)
1725-                    bucketwriters[shnum] = bw
1726-                    self._active_writers[bw] = 1
1727-                    if limited:
1728-                        remaining_space -= max_space_per_bucket
1729-                else:
1730-                    # bummer! not enough space to accept this bucket
1731-                    pass
1732+        for shnum in (sharenums - alreadygot):
1733+            if (not limited) or (remaining_space >= max_space_per_bucket):
1734+                #XXX or should the following line occur in storage server construtor? ok! we need to create the new share file.
1735+                self.backend.set_storage_server(self)
1736+                bw = self.backend.make_bucket_writer(storage_index, shnum,
1737+                                                     max_space_per_bucket, lease_info, canary)
1738+                bucketwriters[shnum] = bw
1739+                self._active_writers[bw] = 1
1740+                if limited:
1741+                    remaining_space -= max_space_per_bucket
1742 
1743hunk ./src/allmydata/storage/server.py 232
1744-            elif share.is_complete():
1745-                # great! we already have it. easy.
1746-                pass
1747-            elif not share.is_complete():
1748-                # Note that we don't create BucketWriters for shnums that
1749-                # have a partial share (in incoming/), so if a second upload
1750-                # occurs while the first is still in progress, the second
1751-                # uploader will use different storage servers.
1752-                pass
1753+        #XXX We SHOULD DOCUMENT LATER.
1754 
1755         self.add_latency("allocate", time.time() - start)
1756         return alreadygot, bucketwriters
1757hunk ./src/allmydata/storage/server.py 238
1758 
1759     def _iter_share_files(self, storage_index):
1760-        for shnum, filename in self._get_bucket_shares(storage_index):
1761+        for shnum, filename in self._get_shares(storage_index):
1762             f = open(filename, 'rb')
1763             header = f.read(32)
1764             f.close()
1765hunk ./src/allmydata/storage/server.py 318
1766         si_s = si_b2a(storage_index)
1767         log.msg("storage: get_buckets %s" % si_s)
1768         bucketreaders = {} # k: sharenum, v: BucketReader
1769-        for shnum, filename in self.backend.get_bucket_shares(storage_index):
1770+        for shnum, filename in self.backend.get_shares(storage_index):
1771             bucketreaders[shnum] = BucketReader(self, filename,
1772                                                 storage_index, shnum)
1773         self.add_latency("get", time.time() - start)
1774hunk ./src/allmydata/storage/server.py 334
1775         # since all shares get the same lease data, we just grab the leases
1776         # from the first share
1777         try:
1778-            shnum, filename = self._get_bucket_shares(storage_index).next()
1779+            shnum, filename = self._get_shares(storage_index).next()
1780             sf = ShareFile(filename)
1781             return sf.get_leases()
1782         except StopIteration:
1783hunk ./src/allmydata/storage/shares.py 1
1784-#! /usr/bin/python
1785-
1786-from allmydata.storage.mutable import MutableShareFile
1787-from allmydata.storage.immutable import ShareFile
1788-
1789-def get_share_file(filename):
1790-    f = open(filename, "rb")
1791-    prefix = f.read(32)
1792-    f.close()
1793-    if prefix == MutableShareFile.MAGIC:
1794-        return MutableShareFile(filename)
1795-    # otherwise assume it's immutable
1796-    return ShareFile(filename)
1797-
1798rmfile ./src/allmydata/storage/shares.py
1799hunk ./src/allmydata/test/common_util.py 20
1800 
1801 def flip_one_bit(s, offset=0, size=None):
1802     """ flip one random bit of the string s, in a byte greater than or equal to offset and less
1803-    than offset+size. """
1804+    than offset+size. Return the new string. """
1805     if size is None:
1806         size=len(s)-offset
1807     i = randrange(offset, offset+size)
1808hunk ./src/allmydata/test/test_backends.py 7
1809 
1810 from allmydata.test.common_util import ReallyEqualMixin
1811 
1812-import mock
1813+import mock, os
1814 
1815 # This is the code that we're going to be testing.
1816hunk ./src/allmydata/test/test_backends.py 10
1817-from allmydata.storage.server import StorageServer, FSBackend, NullBackend
1818+from allmydata.storage.server import StorageServer
1819+
1820+from allmydata.storage.backends.das.core import DASCore
1821+from allmydata.storage.backends.null.core import NullCore
1822+
1823 
1824 # The following share file contents was generated with
1825 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
1826hunk ./src/allmydata/test/test_backends.py 22
1827 share_data = 'a\x00\x00\x00\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\x00(\xde\x80'
1828 share_file_data = '\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01' + share_data
1829 
1830-sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
1831+tempdir = 'teststoredir'
1832+sharedirname = os.path.join(tempdir, 'shares', 'or', 'orsxg5dtorxxeylhmvpws3temv4a')
1833+sharefname = os.path.join(sharedirname, '0')
1834 
1835 class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
1836     @mock.patch('time.time')
1837hunk ./src/allmydata/test/test_backends.py 58
1838         filesystem in only the prescribed ways. """
1839 
1840         def call_open(fname, mode):
1841-            if fname == 'testdir/bucket_counter.state':
1842-                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
1843-            elif fname == 'testdir/lease_checker.state':
1844-                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
1845-            elif fname == 'testdir/lease_checker.history':
1846+            if fname == os.path.join(tempdir,'bucket_counter.state'):
1847+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'bucket_counter.state'))
1848+            elif fname == os.path.join(tempdir, 'lease_checker.state'):
1849+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'lease_checker.state'))
1850+            elif fname == os.path.join(tempdir, 'lease_checker.history'):
1851                 return StringIO()
1852             else:
1853                 self.fail("Server with FS backend tried to open '%s' in mode '%s'" % (fname, mode))
1854hunk ./src/allmydata/test/test_backends.py 124
1855     @mock.patch('__builtin__.open')
1856     def setUp(self, mockopen):
1857         def call_open(fname, mode):
1858-            if fname == 'testdir/bucket_counter.state':
1859-                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
1860-            elif fname == 'testdir/lease_checker.state':
1861-                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
1862-            elif fname == 'testdir/lease_checker.history':
1863+            if fname == os.path.join(tempdir, 'bucket_counter.state'):
1864+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'bucket_counter.state'))
1865+            elif fname == os.path.join(tempdir, 'lease_checker.state'):
1866+                raise IOError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'lease_checker.state'))
1867+            elif fname == os.path.join(tempdir, 'lease_checker.history'):
1868                 return StringIO()
1869         mockopen.side_effect = call_open
1870hunk ./src/allmydata/test/test_backends.py 131
1871-
1872-        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
1873+        expiration_policy = {'enabled' : False,
1874+                             'mode' : 'age',
1875+                             'override_lease_duration' : None,
1876+                             'cutoff_date' : None,
1877+                             'sharetypes' : None}
1878+        testbackend = DASCore(tempdir, expiration_policy)
1879+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=DASCore(tempdir, expiration_policy) )
1880 
1881     @mock.patch('time.time')
1882     @mock.patch('os.mkdir')
1883hunk ./src/allmydata/test/test_backends.py 148
1884         """ Write a new share. """
1885 
1886         def call_listdir(dirname):
1887-            self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
1888-            raise OSError(2, "No such file or directory: 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a'")
1889+            self.failUnlessReallyEqual(dirname, sharedirname)
1890+            raise OSError(2, "No such file or directory: '%s'" % os.path.join(tempdir, 'shares/or/orsxg5dtorxxeylhmvpws3temv4a'))
1891 
1892         mocklistdir.side_effect = call_listdir
1893 
1894hunk ./src/allmydata/test/test_backends.py 178
1895 
1896         sharefile = MockFile()
1897         def call_open(fname, mode):
1898-            self.failUnlessReallyEqual(fname, 'testdir/shares/incoming/or/orsxg5dtorxxeylhmvpws3temv4a/0' )
1899+            self.failUnlessReallyEqual(fname, os.path.join(tempdir, 'shares', 'or', 'orsxg5dtorxxeylhmvpws3temv4a', '0' ))
1900             return sharefile
1901 
1902         mockopen.side_effect = call_open
1903hunk ./src/allmydata/test/test_backends.py 200
1904         StorageServer object. """
1905 
1906         def call_listdir(dirname):
1907-            self.failUnlessReallyEqual(dirname,'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
1908+            self.failUnlessReallyEqual(dirname, os.path.join(tempdir, 'shares', 'or', 'orsxg5dtorxxeylhmvpws3temv4a'))
1909             return ['0']
1910 
1911         mocklistdir.side_effect = call_listdir
1912}
1913[checkpoint patch
1914wilcoxjg@gmail.com**20110626165715
1915 Ignore-this: fbfce2e8a1c1bb92715793b8ad6854d5
1916] {
1917hunk ./src/allmydata/storage/backends/das/core.py 21
1918 from allmydata.storage.lease import LeaseInfo
1919 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
1920      create_mutable_sharefile
1921-from allmydata.storage.backends.das.immutable import NullBucketWriter, BucketWriter, BucketReader
1922+from allmydata.storage.immutable import BucketWriter, BucketReader
1923 from allmydata.storage.crawler import FSBucketCountingCrawler
1924 from allmydata.storage.backends.das.expirer import FSLeaseCheckingCrawler
1925 
1926hunk ./src/allmydata/storage/backends/das/core.py 27
1927 from zope.interface import implements
1928 
1929+# $SHARENUM matches this regex:
1930+NUM_RE=re.compile("^[0-9]+$")
1931+
1932 class DASCore(Backend):
1933     implements(IStorageBackend)
1934     def __init__(self, storedir, expiration_policy, readonly=False, reserved_space=0):
1935hunk ./src/allmydata/storage/backends/das/core.py 80
1936         return fileutil.get_available_space(self.storedir, self.reserved_space)
1937 
1938     def get_shares(self, storage_index):
1939-        """Return a list of the FSBShare objects that correspond to the passed storage_index."""
1940+        """Return a list of the ImmutableShare objects that correspond to the passed storage_index."""
1941         finalstoragedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
1942         try:
1943             for f in os.listdir(finalstoragedir):
1944hunk ./src/allmydata/storage/backends/das/core.py 86
1945                 if NUM_RE.match(f):
1946                     filename = os.path.join(finalstoragedir, f)
1947-                    yield FSBShare(filename, int(f))
1948+                    yield ImmutableShare(self.sharedir, storage_index, int(f))
1949         except OSError:
1950             # Commonly caused by there being no buckets at all.
1951             pass
1952hunk ./src/allmydata/storage/backends/das/core.py 95
1953         immsh = ImmutableShare(self.sharedir, storage_index, shnum, max_size=max_space_per_bucket, create=True)
1954         bw = BucketWriter(self.ss, immsh, max_space_per_bucket, lease_info, canary)
1955         return bw
1956+
1957+    def set_storage_server(self, ss):
1958+        self.ss = ss
1959         
1960 
1961 # each share file (in storage/shares/$SI/$SHNUM) contains lease information
1962hunk ./src/allmydata/storage/server.py 29
1963 # Where "$START" denotes the first 10 bits worth of $STORAGEINDEX (that's 2
1964 # base-32 chars).
1965 
1966-# $SHARENUM matches this regex:
1967-NUM_RE=re.compile("^[0-9]+$")
1968 
1969 class StorageServer(service.MultiService, Referenceable):
1970     implements(RIStorageServer, IStatsProducer)
1971}
1972
1973Context:
1974
1975[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
1976david-sarah@jacaranda.org**20110623205528
1977 Ignore-this: c63e23146c39195de52fb17c7c49b2da
1978]
1979[Rename test_package_initialization.py to (much shorter) test_import.py .
1980Brian Warner <warner@lothar.com>**20110611190234
1981 Ignore-this: 3eb3dbac73600eeff5cfa6b65d65822
1982 
1983 The former name was making my 'ls' listings hard to read, by forcing them
1984 down to just two columns.
1985]
1986[tests: fix tests to accomodate [20110611153758-92b7f-0ba5e4726fb6318dac28fb762a6512a003f4c430]
1987zooko@zooko.com**20110611163741
1988 Ignore-this: 64073a5f39e7937e8e5e1314c1a302d1
1989 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.
1990 fixes #1412
1991]
1992[wui: right-align the size column in the WUI
1993zooko@zooko.com**20110611153758
1994 Ignore-this: 492bdaf4373c96f59f90581c7daf7cd7
1995 Thanks to Ted "stercor" Rolle Jr. and Terrell Russell.
1996 fixes #1412
1997]
1998[docs: three minor fixes
1999zooko@zooko.com**20110610121656
2000 Ignore-this: fec96579eb95aceb2ad5fc01a814c8a2
2001 CREDITS for arc for stats tweak
2002 fix link to .zip file in quickstart.rst (thanks to ChosenOne for noticing)
2003 English usage tweak
2004]
2005[docs/running.rst: fix stray HTML (not .rst) link noticed by ChosenOne.
2006david-sarah@jacaranda.org**20110609223719
2007 Ignore-this: fc50ac9c94792dcac6f1067df8ac0d4a
2008]
2009[server.py:  get_latencies now reports percentiles _only_ if there are sufficient observations for the interpretation of the percentile to be unambiguous.
2010wilcoxjg@gmail.com**20110527120135
2011 Ignore-this: 2e7029764bffc60e26f471d7c2b6611e
2012 interfaces.py:  modified the return type of RIStatsProvider.get_stats to allow for None as a return value
2013 NEWS.rst, stats.py: documentation of change to get_latencies
2014 stats.rst: now documents percentile modification in get_latencies
2015 test_storage.py:  test_latencies now expects None in output categories that contain too few samples for the associated percentile to be unambiguously reported.
2016 fixes #1392
2017]
2018[docs: revert link in relnotes.txt from NEWS.rst to NEWS, since the former did not exist at revision 5000.
2019david-sarah@jacaranda.org**20110517011214
2020 Ignore-this: 6a5be6e70241e3ec0575641f64343df7
2021]
2022[docs: convert NEWS to NEWS.rst and change all references to it.
2023david-sarah@jacaranda.org**20110517010255
2024 Ignore-this: a820b93ea10577c77e9c8206dbfe770d
2025]
2026[docs: remove out-of-date docs/testgrid/introducer.furl and containing directory. fixes #1404
2027david-sarah@jacaranda.org**20110512140559
2028 Ignore-this: 784548fc5367fac5450df1c46890876d
2029]
2030[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
2031david-sarah@jacaranda.org**20110130164923
2032 Ignore-this: a271e77ce81d84bb4c43645b891d92eb
2033]
2034[setup: don't catch all Exception from check_requirement(), but only PackagingError and ImportError
2035zooko@zooko.com**20110128142006
2036 Ignore-this: 57d4bc9298b711e4bc9dc832c75295de
2037 I noticed this because I had accidentally inserted a bug which caused AssertionError to be raised from check_requirement().
2038]
2039[M-x whitespace-cleanup
2040zooko@zooko.com**20110510193653
2041 Ignore-this: dea02f831298c0f65ad096960e7df5c7
2042]
2043[docs: fix typo in running.rst, thanks to arch_o_median
2044zooko@zooko.com**20110510193633
2045 Ignore-this: ca06de166a46abbc61140513918e79e8
2046]
2047[relnotes.txt: don't claim to work on Cygwin (which has been untested for some time). refs #1342
2048david-sarah@jacaranda.org**20110204204902
2049 Ignore-this: 85ef118a48453d93fa4cddc32d65b25b
2050]
2051[relnotes.txt: forseeable -> foreseeable. refs #1342
2052david-sarah@jacaranda.org**20110204204116
2053 Ignore-this: 746debc4d82f4031ebf75ab4031b3a9
2054]
2055[replace remaining .html docs with .rst docs
2056zooko@zooko.com**20110510191650
2057 Ignore-this: d557d960a986d4ac8216d1677d236399
2058 Remove install.html (long since deprecated).
2059 Also replace some obsolete references to install.html with references to quickstart.rst.
2060 Fix some broken internal references within docs/historical/historical_known_issues.txt.
2061 Thanks to Ravi Pinjala and Patrick McDonald.
2062 refs #1227
2063]
2064[docs: FTP-and-SFTP.rst: fix a minor error and update the information about which version of Twisted fixes #1297
2065zooko@zooko.com**20110428055232
2066 Ignore-this: b63cfb4ebdbe32fb3b5f885255db4d39
2067]
2068[munin tahoe_files plugin: fix incorrect file count
2069francois@ctrlaltdel.ch**20110428055312
2070 Ignore-this: 334ba49a0bbd93b4a7b06a25697aba34
2071 fixes #1391
2072]
2073[corrected "k must never be smaller than N" to "k must never be greater than N"
2074secorp@allmydata.org**20110425010308
2075 Ignore-this: 233129505d6c70860087f22541805eac
2076]
2077[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
2078david-sarah@jacaranda.org**20110411190738
2079 Ignore-this: 7847d26bc117c328c679f08a7baee519
2080]
2081[tests: add test for including the ImportError message and traceback entry in the summary of errors from importing dependencies. refs #1389
2082david-sarah@jacaranda.org**20110410155844
2083 Ignore-this: fbecdbeb0d06a0f875fe8d4030aabafa
2084]
2085[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
2086david-sarah@jacaranda.org**20110410155705
2087 Ignore-this: 2f87b8b327906cf8bfca9440a0904900
2088]
2089[remove unused variable detected by pyflakes
2090zooko@zooko.com**20110407172231
2091 Ignore-this: 7344652d5e0720af822070d91f03daf9
2092]
2093[allmydata/__init__.py: Nicer reporting of unparseable version numbers in dependencies. fixes #1388
2094david-sarah@jacaranda.org**20110401202750
2095 Ignore-this: 9c6bd599259d2405e1caadbb3e0d8c7f
2096]
2097[update FTP-and-SFTP.rst: the necessary patch is included in Twisted-10.1
2098Brian Warner <warner@lothar.com>**20110325232511
2099 Ignore-this: d5307faa6900f143193bfbe14e0f01a
2100]
2101[control.py: remove all uses of s.get_serverid()
2102warner@lothar.com**20110227011203
2103 Ignore-this: f80a787953bd7fa3d40e828bde00e855
2104]
2105[web: remove some uses of s.get_serverid(), not all
2106warner@lothar.com**20110227011159
2107 Ignore-this: a9347d9cf6436537a47edc6efde9f8be
2108]
2109[immutable/downloader/fetcher.py: remove all get_serverid() calls
2110warner@lothar.com**20110227011156
2111 Ignore-this: fb5ef018ade1749348b546ec24f7f09a
2112]
2113[immutable/downloader/fetcher.py: fix diversity bug in server-response handling
2114warner@lothar.com**20110227011153
2115 Ignore-this: bcd62232c9159371ae8a16ff63d22c1b
2116 
2117 When blocks terminate (either COMPLETE or CORRUPT/DEAD/BADSEGNUM), the
2118 _shares_from_server dict was being popped incorrectly (using shnum as the
2119 index instead of serverid). I'm still thinking through the consequences of
2120 this bug. It was probably benign and really hard to detect. I think it would
2121 cause us to incorrectly believe that we're pulling too many shares from a
2122 server, and thus prefer a different server rather than asking for a second
2123 share from the first server. The diversity code is intended to spread out the
2124 number of shares simultaneously being requested from each server, but with
2125 this bug, it might be spreading out the total number of shares requested at
2126 all, not just simultaneously. (note that SegmentFetcher is scoped to a single
2127 segment, so the effect doesn't last very long).
2128]
2129[immutable/downloader/share.py: reduce get_serverid(), one left, update ext deps
2130warner@lothar.com**20110227011150
2131 Ignore-this: d8d56dd8e7b280792b40105e13664554
2132 
2133 test_download.py: create+check MyShare instances better, make sure they share
2134 Server objects, now that finder.py cares
2135]
2136[immutable/downloader/finder.py: reduce use of get_serverid(), one left
2137warner@lothar.com**20110227011146
2138 Ignore-this: 5785be173b491ae8a78faf5142892020
2139]
2140[immutable/offloaded.py: reduce use of get_serverid() a bit more
2141warner@lothar.com**20110227011142
2142 Ignore-this: b48acc1b2ae1b311da7f3ba4ffba38f
2143]
2144[immutable/upload.py: reduce use of get_serverid()
2145warner@lothar.com**20110227011138
2146 Ignore-this: ffdd7ff32bca890782119a6e9f1495f6
2147]
2148[immutable/checker.py: remove some uses of s.get_serverid(), not all
2149warner@lothar.com**20110227011134
2150 Ignore-this: e480a37efa9e94e8016d826c492f626e
2151]
2152[add remaining get_* methods to storage_client.Server, NoNetworkServer, and
2153warner@lothar.com**20110227011132
2154 Ignore-this: 6078279ddf42b179996a4b53bee8c421
2155 MockIServer stubs
2156]
2157[upload.py: rearrange _make_trackers a bit, no behavior changes
2158warner@lothar.com**20110227011128
2159 Ignore-this: 296d4819e2af452b107177aef6ebb40f
2160]
2161[happinessutil.py: finally rename merge_peers to merge_servers
2162warner@lothar.com**20110227011124
2163 Ignore-this: c8cd381fea1dd888899cb71e4f86de6e
2164]
2165[test_upload.py: factor out FakeServerTracker
2166warner@lothar.com**20110227011120
2167 Ignore-this: 6c182cba90e908221099472cc159325b
2168]
2169[test_upload.py: server-vs-tracker cleanup
2170warner@lothar.com**20110227011115
2171 Ignore-this: 2915133be1a3ba456e8603885437e03
2172]
2173[happinessutil.py: server-vs-tracker cleanup
2174warner@lothar.com**20110227011111
2175 Ignore-this: b856c84033562d7d718cae7cb01085a9
2176]
2177[upload.py: more tracker-vs-server cleanup
2178warner@lothar.com**20110227011107
2179 Ignore-this: bb75ed2afef55e47c085b35def2de315
2180]
2181[upload.py: fix var names to avoid confusion between 'trackers' and 'servers'
2182warner@lothar.com**20110227011103
2183 Ignore-this: 5d5e3415b7d2732d92f42413c25d205d
2184]
2185[refactor: s/peer/server/ in immutable/upload, happinessutil.py, test_upload
2186warner@lothar.com**20110227011100
2187 Ignore-this: 7ea858755cbe5896ac212a925840fe68
2188 
2189 No behavioral changes, just updating variable/method names and log messages.
2190 The effects outside these three files should be minimal: some exception
2191 messages changed (to say "server" instead of "peer"), and some internal class
2192 names were changed. A few things still use "peer" to minimize external
2193 changes, like UploadResults.timings["peer_selection"] and
2194 happinessutil.merge_peers, which can be changed later.
2195]
2196[storage_client.py: clean up test_add_server/test_add_descriptor, remove .test_servers
2197warner@lothar.com**20110227011056
2198 Ignore-this: efad933e78179d3d5fdcd6d1ef2b19cc
2199]
2200[test_client.py, upload.py:: remove KiB/MiB/etc constants, and other dead code
2201warner@lothar.com**20110227011051
2202 Ignore-this: dc83c5794c2afc4f81e592f689c0dc2d
2203]
2204[test: increase timeout on a network test because Francois's ARM machine hit that timeout
2205zooko@zooko.com**20110317165909
2206 Ignore-this: 380c345cdcbd196268ca5b65664ac85b
2207 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.
2208]
2209[docs/configuration.rst: add a "Frontend Configuration" section
2210Brian Warner <warner@lothar.com>**20110222014323
2211 Ignore-this: 657018aa501fe4f0efef9851628444ca
2212 
2213 this points to docs/frontends/*.rst, which were previously underlinked
2214]
2215[web/filenode.py: avoid calling req.finish() on closed HTTP connections. Closes #1366
2216"Brian Warner <warner@lothar.com>"**20110221061544
2217 Ignore-this: 799d4de19933f2309b3c0c19a63bb888
2218]
2219[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.
2220david-sarah@jacaranda.org**20110221015817
2221 Ignore-this: 51d181698f8c20d3aca58b057e9c475a
2222]
2223[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.
2224david-sarah@jacaranda.org**20110221020125
2225 Ignore-this: b0744ed58f161bf188e037bad077fc48
2226]
2227[Refactor StorageFarmBroker handling of servers
2228Brian Warner <warner@lothar.com>**20110221015804
2229 Ignore-this: 842144ed92f5717699b8f580eab32a51
2230 
2231 Pass around IServer instance instead of (peerid, rref) tuple. Replace
2232 "descriptor" with "server". Other replacements:
2233 
2234  get_all_servers -> get_connected_servers/get_known_servers
2235  get_servers_for_index -> get_servers_for_psi (now returns IServers)
2236 
2237 This change still needs to be pushed further down: lots of code is now
2238 getting the IServer and then distributing (peerid, rref) internally.
2239 Instead, it ought to distribute the IServer internally and delay
2240 extracting a serverid or rref until the last moment.
2241 
2242 no_network.py was updated to retain parallelism.
2243]
2244[TAG allmydata-tahoe-1.8.2
2245warner@lothar.com**20110131020101]
2246Patch bundle hash:
2247d444f31d63986c595a0c9f3787c645cc5d260ae9