Ticket #999: workingonbackend01.darcs.patch

File workingonbackend01.darcs.patch, 45.5 KB (added by arch_o_median, at 2011-06-24T20:32:00Z)

Implements tests of read and write for the nullbackend

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
9New patches:
10
11[storage: new mocking tests of storage server read and write
12wilcoxjg@gmail.com**20110325203514
13 Ignore-this: df65c3c4f061dd1516f88662023fdb41
14 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.
15] {
16addfile ./src/allmydata/test/test_server.py
17hunk ./src/allmydata/test/test_server.py 1
18+from twisted.trial import unittest
19+
20+from StringIO import StringIO
21+
22+from allmydata.test.common_util import ReallyEqualMixin
23+
24+import mock
25+
26+# This is the code that we're going to be testing.
27+from allmydata.storage.server import StorageServer
28+
29+# The following share file contents was generated with
30+# storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
31+# with share data == 'a'.
32+share_data = 'a\x00\x00\x00\x00xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy\x00(\xde\x80'
33+share_file_data = '\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x01' + share_data
34+
35+sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
36+
37+class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
38+    @mock.patch('__builtin__.open')
39+    def test_create_server(self, mockopen):
40+        """ This tests whether a server instance can be constructed. """
41+
42+        def call_open(fname, mode):
43+            if fname == 'testdir/bucket_counter.state':
44+                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
45+            elif fname == 'testdir/lease_checker.state':
46+                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
47+            elif fname == 'testdir/lease_checker.history':
48+                return StringIO()
49+        mockopen.side_effect = call_open
50+
51+        # Now begin the test.
52+        s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
53+
54+        # You passed!
55+
56+class TestServer(unittest.TestCase, ReallyEqualMixin):
57+    @mock.patch('__builtin__.open')
58+    def setUp(self, mockopen):
59+        def call_open(fname, mode):
60+            if fname == 'testdir/bucket_counter.state':
61+                raise IOError(2, "No such file or directory: 'testdir/bucket_counter.state'")
62+            elif fname == 'testdir/lease_checker.state':
63+                raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
64+            elif fname == 'testdir/lease_checker.history':
65+                return StringIO()
66+        mockopen.side_effect = call_open
67+
68+        self.s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
69+
70+
71+    @mock.patch('time.time')
72+    @mock.patch('os.mkdir')
73+    @mock.patch('__builtin__.open')
74+    @mock.patch('os.listdir')
75+    @mock.patch('os.path.isdir')
76+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
77+        """Handle a report of corruption."""
78+
79+        def call_listdir(dirname):
80+            self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
81+            raise OSError(2, "No such file or directory: 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a'")
82+
83+        mocklistdir.side_effect = call_listdir
84+
85+        class MockFile:
86+            def __init__(self):
87+                self.buffer = ''
88+                self.pos = 0
89+            def write(self, instring):
90+                begin = self.pos
91+                padlen = begin - len(self.buffer)
92+                if padlen > 0:
93+                    self.buffer += '\x00' * padlen
94+                end = self.pos + len(instring)
95+                self.buffer = self.buffer[:begin]+instring+self.buffer[end:]
96+                self.pos = end
97+            def close(self):
98+                pass
99+            def seek(self, pos):
100+                self.pos = pos
101+            def read(self, numberbytes):
102+                return self.buffer[self.pos:self.pos+numberbytes]
103+            def tell(self):
104+                return self.pos
105+
106+        mocktime.return_value = 0
107+
108+        sharefile = MockFile()
109+        def call_open(fname, mode):
110+            self.failUnlessReallyEqual(fname, 'testdir/shares/incoming/or/orsxg5dtorxxeylhmvpws3temv4a/0' )
111+            return sharefile
112+
113+        mockopen.side_effect = call_open
114+        # Now begin the test.
115+        alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
116+        print bs
117+        bs[0].remote_write(0, 'a')
118+        self.failUnlessReallyEqual(sharefile.buffer, share_file_data)
119+
120+
121+    @mock.patch('os.path.exists')
122+    @mock.patch('os.path.getsize')
123+    @mock.patch('__builtin__.open')
124+    @mock.patch('os.listdir')
125+    def test_read_share(self, mocklistdir, mockopen, mockgetsize, mockexists):
126+        """ This tests whether the code correctly finds and reads
127+        shares written out by old (Tahoe-LAFS <= v1.8.2)
128+        servers. There is a similar test in test_download, but that one
129+        is from the perspective of the client and exercises a deeper
130+        stack of code. This one is for exercising just the
131+        StorageServer object. """
132+
133+        def call_listdir(dirname):
134+            self.failUnlessReallyEqual(dirname,'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
135+            return ['0']
136+
137+        mocklistdir.side_effect = call_listdir
138+
139+        def call_open(fname, mode):
140+            self.failUnlessReallyEqual(fname, sharefname)
141+            self.failUnless('r' in mode, mode)
142+            self.failUnless('b' in mode, mode)
143+
144+            return StringIO(share_file_data)
145+        mockopen.side_effect = call_open
146+
147+        datalen = len(share_file_data)
148+        def call_getsize(fname):
149+            self.failUnlessReallyEqual(fname, sharefname)
150+            return datalen
151+        mockgetsize.side_effect = call_getsize
152+
153+        def call_exists(fname):
154+            self.failUnlessReallyEqual(fname, sharefname)
155+            return True
156+        mockexists.side_effect = call_exists
157+
158+        # Now begin the test.
159+        bs = self.s.remote_get_buckets('teststorage_index')
160+
161+        self.failUnlessEqual(len(bs), 1)
162+        b = bs[0]
163+        self.failUnlessReallyEqual(b.remote_read(0, datalen), share_data)
164+        # If you try to read past the end you get the as much data as is there.
165+        self.failUnlessReallyEqual(b.remote_read(0, datalen+20), share_data)
166+        # If you start reading past the end of the file you get the empty string.
167+        self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
168}
169[server.py, test_backends.py, interfaces.py, immutable.py (others?): working patch for implementation of backends plugin
170wilcoxjg@gmail.com**20110624202850
171 Ignore-this: ca6f34987ee3b0d25cac17c1fc22d50c
172 sloppy not for production
173] {
174move ./src/allmydata/test/test_server.py ./src/allmydata/test/test_backends.py
175hunk ./src/allmydata/storage/crawler.py 13
176     pass
177 
178 class ShareCrawler(service.MultiService):
179-    """A ShareCrawler subclass is attached to a StorageServer, and
180+    """A subcless of ShareCrawler is attached to a StorageServer, and
181     periodically walks all of its shares, processing each one in some
182     fashion. This crawl is rate-limited, to reduce the IO burden on the host,
183     since large servers can easily have a terabyte of shares, in several
184hunk ./src/allmydata/storage/crawler.py 31
185     We assume that the normal upload/download/get_buckets traffic of a tahoe
186     grid will cause the prefixdir contents to be mostly cached in the kernel,
187     or that the number of buckets in each prefixdir will be small enough to
188-    load quickly. A 1TB allmydata.com server was measured to have 2.56M
189+    load quickly. A 1TB allmydata.com server was measured to have 2.56 * 10^6
190     buckets, spread into the 1024 prefixdirs, with about 2500 buckets per
191     prefix. On this server, each prefixdir took 130ms-200ms to list the first
192     time, and 17ms to list the second time.
193hunk ./src/allmydata/storage/crawler.py 68
194     cpu_slice = 1.0 # use up to 1.0 seconds before yielding
195     minimum_cycle_time = 300 # don't run a cycle faster than this
196 
197-    def __init__(self, server, statefile, allowed_cpu_percentage=None):
198+    def __init__(self, backend, statefile, allowed_cpu_percentage=None):
199         service.MultiService.__init__(self)
200         if allowed_cpu_percentage is not None:
201             self.allowed_cpu_percentage = allowed_cpu_percentage
202hunk ./src/allmydata/storage/crawler.py 72
203-        self.server = server
204-        self.sharedir = server.sharedir
205-        self.statefile = statefile
206+        self.backend = backend
207         self.prefixes = [si_b2a(struct.pack(">H", i << (16-10)))[:2]
208                          for i in range(2**10)]
209         self.prefixes.sort()
210hunk ./src/allmydata/storage/crawler.py 446
211 
212     minimum_cycle_time = 60*60 # we don't need this more than once an hour
213 
214-    def __init__(self, server, statefile, num_sample_prefixes=1):
215-        ShareCrawler.__init__(self, server, statefile)
216+    def __init__(self, statefile, num_sample_prefixes=1):
217+        ShareCrawler.__init__(self, statefile)
218         self.num_sample_prefixes = num_sample_prefixes
219 
220     def add_initial_state(self):
221hunk ./src/allmydata/storage/expirer.py 15
222     removed.
223 
224     I collect statistics on the leases and make these available to a web
225-    status page, including::
226+    status page, including:
227 
228     Space recovered during this cycle-so-far:
229      actual (only if expiration_enabled=True):
230hunk ./src/allmydata/storage/expirer.py 51
231     slow_start = 360 # wait 6 minutes after startup
232     minimum_cycle_time = 12*60*60 # not more than twice per day
233 
234-    def __init__(self, server, statefile, historyfile,
235+    def __init__(self, statefile, historyfile,
236                  expiration_enabled, mode,
237                  override_lease_duration, # used if expiration_mode=="age"
238                  cutoff_date, # used if expiration_mode=="cutoff-date"
239hunk ./src/allmydata/storage/expirer.py 71
240         else:
241             raise ValueError("GC mode '%s' must be 'age' or 'cutoff-date'" % mode)
242         self.sharetypes_to_expire = sharetypes
243-        ShareCrawler.__init__(self, server, statefile)
244+        ShareCrawler.__init__(self, statefile)
245 
246     def add_initial_state(self):
247         # we fill ["cycle-to-date"] here (even though they will be reset in
248hunk ./src/allmydata/storage/immutable.py 44
249     sharetype = "immutable"
250 
251     def __init__(self, filename, max_size=None, create=False):
252-        """ 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. """
253+        """ If max_size is not None then I won't allow more than
254+        max_size to be written to me. If create=True then max_size
255+        must not be None. """
256         precondition((max_size is not None) or (not create), max_size, create)
257         self.home = filename
258         self._max_size = max_size
259hunk ./src/allmydata/storage/immutable.py 87
260 
261     def read_share_data(self, offset, length):
262         precondition(offset >= 0)
263-        # reads beyond the end of the data are truncated. Reads that start
264-        # beyond the end of the data return an empty string. I wonder why
265-        # Python doesn't do the following computation for me?
266+        # Reads beyond the end of the data are truncated. Reads that start
267+        # beyond the end of the data return an empty string.
268         seekpos = self._data_offset+offset
269         fsize = os.path.getsize(self.home)
270         actuallength = max(0, min(length, fsize-seekpos))
271hunk ./src/allmydata/storage/immutable.py 198
272             space_freed += os.stat(self.home)[stat.ST_SIZE]
273             self.unlink()
274         return space_freed
275+class NullBucketWriter(Referenceable):
276+    implements(RIBucketWriter)
277 
278hunk ./src/allmydata/storage/immutable.py 201
279+    def remote_write(self, offset, data):
280+        return
281 
282 class BucketWriter(Referenceable):
283     implements(RIBucketWriter)
284hunk ./src/allmydata/storage/server.py 7
285 from twisted.application import service
286 
287 from zope.interface import implements
288-from allmydata.interfaces import RIStorageServer, IStatsProducer
289+from allmydata.interfaces import RIStorageServer, IStatsProducer, IShareStore
290 from allmydata.util import fileutil, idlib, log, time_format
291 import allmydata # for __full_version__
292 
293hunk ./src/allmydata/storage/server.py 16
294 from allmydata.storage.lease import LeaseInfo
295 from allmydata.storage.mutable import MutableShareFile, EmptyShare, \
296      create_mutable_sharefile
297-from allmydata.storage.immutable import ShareFile, BucketWriter, BucketReader
298+from allmydata.storage.immutable import ShareFile, NullBucketWriter, BucketWriter, BucketReader
299 from allmydata.storage.crawler import BucketCountingCrawler
300 from allmydata.storage.expirer import LeaseCheckingCrawler
301 
302hunk ./src/allmydata/storage/server.py 20
303+from zope.interface import implements
304+
305+# A Backend is a MultiService so that its server's crawlers (if the server has any) can
306+# be started and stopped.
307+class Backend(service.MultiService):
308+    implements(IStatsProducer)
309+    def __init__(self):
310+        service.MultiService.__init__(self)
311+
312+    def get_bucket_shares(self):
313+        """XXX"""
314+        raise NotImplementedError
315+
316+    def get_share(self):
317+        """XXX"""
318+        raise NotImplementedError
319+
320+    def make_bucket_writer(self):
321+        """XXX"""
322+        raise NotImplementedError
323+
324+class NullBackend(Backend):
325+    def __init__(self):
326+        Backend.__init__(self)
327+
328+    def get_available_space(self):
329+        return None
330+
331+    def get_bucket_shares(self, storage_index):
332+        return set()
333+
334+    def get_share(self, storage_index, sharenum):
335+        return None
336+
337+    def make_bucket_writer(self, storage_index, shnum, max_space_per_bucket, lease_info, canary):
338+        return NullBucketWriter()
339+
340+class FSBackend(Backend):
341+    def __init__(self, storedir, readonly=False, reserved_space=0):
342+        Backend.__init__(self)
343+
344+        self._setup_storage(storedir, readonly, reserved_space)
345+        self._setup_corruption_advisory()
346+        self._setup_bucket_counter()
347+        self._setup_lease_checkerf()
348+
349+    def _setup_storage(self, storedir, readonly, reserved_space):
350+        self.storedir = storedir
351+        self.readonly = readonly
352+        self.reserved_space = int(reserved_space)
353+        if self.reserved_space:
354+            if self.get_available_space() is None:
355+                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",
356+                        umid="0wZ27w", level=log.UNUSUAL)
357+
358+        self.sharedir = os.path.join(self.storedir, "shares")
359+        fileutil.make_dirs(self.sharedir)
360+        self.incomingdir = os.path.join(self.sharedir, 'incoming')
361+        self._clean_incomplete()
362+
363+    def _clean_incomplete(self):
364+        fileutil.rm_dir(self.incomingdir)
365+        fileutil.make_dirs(self.incomingdir)
366+
367+    def _setup_corruption_advisory(self):
368+        # we don't actually create the corruption-advisory dir until necessary
369+        self.corruption_advisory_dir = os.path.join(self.storedir,
370+                                                    "corruption-advisories")
371+
372+    def _setup_bucket_counter(self):
373+        statefile = os.path.join(self.storedir, "bucket_counter.state")
374+        self.bucket_counter = BucketCountingCrawler(statefile)
375+        self.bucket_counter.setServiceParent(self)
376+
377+    def _setup_lease_checkerf(self):
378+        statefile = os.path.join(self.storedir, "lease_checker.state")
379+        historyfile = os.path.join(self.storedir, "lease_checker.history")
380+        self.lease_checker = LeaseCheckingCrawler(statefile, historyfile,
381+                                   expiration_enabled, expiration_mode,
382+                                   expiration_override_lease_duration,
383+                                   expiration_cutoff_date,
384+                                   expiration_sharetypes)
385+        self.lease_checker.setServiceParent(self)
386+
387+    def get_available_space(self):
388+        if self.readonly:
389+            return 0
390+        return fileutil.get_available_space(self.storedir, self.reserved_space)
391+
392+    def get_bucket_shares(self, storage_index):
393+        """Return a list of (shnum, pathname) tuples for files that hold
394+        shares for this storage_index. In each tuple, 'shnum' will always be
395+        the integer form of the last component of 'pathname'."""
396+        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
397+        try:
398+            for f in os.listdir(storagedir):
399+                if NUM_RE.match(f):
400+                    filename = os.path.join(storagedir, f)
401+                    yield (int(f), filename)
402+        except OSError:
403+            # Commonly caused by there being no buckets at all.
404+            pass
405+
406 # storage/
407 # storage/shares/incoming
408 #   incoming/ holds temp dirs named $START/$STORAGEINDEX/$SHARENUM which will
409hunk ./src/allmydata/storage/server.py 143
410     name = 'storage'
411     LeaseCheckerClass = LeaseCheckingCrawler
412 
413-    def __init__(self, storedir, nodeid, reserved_space=0,
414-                 discard_storage=False, readonly_storage=False,
415+    def __init__(self, nodeid, backend, reserved_space=0,
416+                 readonly_storage=False,
417                  stats_provider=None,
418                  expiration_enabled=False,
419                  expiration_mode="age",
420hunk ./src/allmydata/storage/server.py 155
421         assert isinstance(nodeid, str)
422         assert len(nodeid) == 20
423         self.my_nodeid = nodeid
424-        self.storedir = storedir
425-        sharedir = os.path.join(storedir, "shares")
426-        fileutil.make_dirs(sharedir)
427-        self.sharedir = sharedir
428-        # we don't actually create the corruption-advisory dir until necessary
429-        self.corruption_advisory_dir = os.path.join(storedir,
430-                                                    "corruption-advisories")
431-        self.reserved_space = int(reserved_space)
432-        self.no_storage = discard_storage
433-        self.readonly_storage = readonly_storage
434         self.stats_provider = stats_provider
435         if self.stats_provider:
436             self.stats_provider.register_producer(self)
437hunk ./src/allmydata/storage/server.py 158
438-        self.incomingdir = os.path.join(sharedir, 'incoming')
439-        self._clean_incomplete()
440-        fileutil.make_dirs(self.incomingdir)
441         self._active_writers = weakref.WeakKeyDictionary()
442hunk ./src/allmydata/storage/server.py 159
443+        self.backend = backend
444+        self.backend.setServiceParent(self)
445         log.msg("StorageServer created", facility="tahoe.storage")
446 
447hunk ./src/allmydata/storage/server.py 163
448-        if reserved_space:
449-            if self.get_available_space() is None:
450-                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",
451-                        umin="0wZ27w", level=log.UNUSUAL)
452-
453         self.latencies = {"allocate": [], # immutable
454                           "write": [],
455                           "close": [],
456hunk ./src/allmydata/storage/server.py 174
457                           "renew": [],
458                           "cancel": [],
459                           }
460-        self.add_bucket_counter()
461-
462-        statefile = os.path.join(self.storedir, "lease_checker.state")
463-        historyfile = os.path.join(self.storedir, "lease_checker.history")
464-        klass = self.LeaseCheckerClass
465-        self.lease_checker = klass(self, statefile, historyfile,
466-                                   expiration_enabled, expiration_mode,
467-                                   expiration_override_lease_duration,
468-                                   expiration_cutoff_date,
469-                                   expiration_sharetypes)
470-        self.lease_checker.setServiceParent(self)
471 
472     def __repr__(self):
473         return "<StorageServer %s>" % (idlib.shortnodeid_b2a(self.my_nodeid),)
474hunk ./src/allmydata/storage/server.py 178
475 
476-    def add_bucket_counter(self):
477-        statefile = os.path.join(self.storedir, "bucket_counter.state")
478-        self.bucket_counter = BucketCountingCrawler(self, statefile)
479-        self.bucket_counter.setServiceParent(self)
480-
481     def count(self, name, delta=1):
482         if self.stats_provider:
483             self.stats_provider.count("storage_server." + name, delta)
484hunk ./src/allmydata/storage/server.py 233
485             kwargs["facility"] = "tahoe.storage"
486         return log.msg(*args, **kwargs)
487 
488-    def _clean_incomplete(self):
489-        fileutil.rm_dir(self.incomingdir)
490-
491     def get_stats(self):
492         # remember: RIStatsProvider requires that our return dict
493         # contains numeric values.
494hunk ./src/allmydata/storage/server.py 269
495             stats['storage_server.total_bucket_count'] = bucket_count
496         return stats
497 
498-    def get_available_space(self):
499-        """Returns available space for share storage in bytes, or None if no
500-        API to get this information is available."""
501-
502-        if self.readonly_storage:
503-            return 0
504-        return fileutil.get_available_space(self.storedir, self.reserved_space)
505-
506     def allocated_size(self):
507         space = 0
508         for bw in self._active_writers:
509hunk ./src/allmydata/storage/server.py 276
510         return space
511 
512     def remote_get_version(self):
513-        remaining_space = self.get_available_space()
514+        remaining_space = self.backend.get_available_space()
515         if remaining_space is None:
516             # We're on a platform that has no API to get disk stats.
517             remaining_space = 2**64
518hunk ./src/allmydata/storage/server.py 301
519         self.count("allocate")
520         alreadygot = set()
521         bucketwriters = {} # k: shnum, v: BucketWriter
522-        si_dir = storage_index_to_dir(storage_index)
523-        si_s = si_b2a(storage_index)
524 
525hunk ./src/allmydata/storage/server.py 302
526+        si_s = si_b2a(storage_index)
527         log.msg("storage: allocate_buckets %s" % si_s)
528 
529         # in this implementation, the lease information (including secrets)
530hunk ./src/allmydata/storage/server.py 316
531 
532         max_space_per_bucket = allocated_size
533 
534-        remaining_space = self.get_available_space()
535+        remaining_space = self.backend.get_available_space()
536         limited = remaining_space is not None
537         if limited:
538             # this is a bit conservative, since some of this allocated_size()
539hunk ./src/allmydata/storage/server.py 329
540         # they asked about: this will save them a lot of work. Add or update
541         # leases for all of them: if they want us to hold shares for this
542         # file, they'll want us to hold leases for this file.
543-        for (shnum, fn) in self._get_bucket_shares(storage_index):
544+        for (shnum, fn) in self.backend.get_bucket_shares(storage_index):
545             alreadygot.add(shnum)
546             sf = ShareFile(fn)
547             sf.add_or_renew_lease(lease_info)
548hunk ./src/allmydata/storage/server.py 335
549 
550         for shnum in sharenums:
551-            incominghome = os.path.join(self.incomingdir, si_dir, "%d" % shnum)
552-            finalhome = os.path.join(self.sharedir, si_dir, "%d" % shnum)
553-            if os.path.exists(finalhome):
554+            share = self.backend.get_share(storage_index, shnum)
555+
556+            if not share:
557+                if (not limited) or (remaining_space >= max_space_per_bucket):
558+                    # ok! we need to create the new share file.
559+                    bw = self.backend.make_bucket_writer(storage_index, shnum,
560+                                      max_space_per_bucket, lease_info, canary)
561+                    bucketwriters[shnum] = bw
562+                    self._active_writers[bw] = 1
563+                    if limited:
564+                        remaining_space -= max_space_per_bucket
565+                else:
566+                    # bummer! not enough space to accept this bucket
567+                    pass
568+
569+            elif share.is_complete():
570                 # great! we already have it. easy.
571                 pass
572hunk ./src/allmydata/storage/server.py 353
573-            elif os.path.exists(incominghome):
574+            elif not share.is_complete():
575                 # Note that we don't create BucketWriters for shnums that
576                 # have a partial share (in incoming/), so if a second upload
577                 # occurs while the first is still in progress, the second
578hunk ./src/allmydata/storage/server.py 359
579                 # uploader will use different storage servers.
580                 pass
581-            elif (not limited) or (remaining_space >= max_space_per_bucket):
582-                # ok! we need to create the new share file.
583-                bw = BucketWriter(self, incominghome, finalhome,
584-                                  max_space_per_bucket, lease_info, canary)
585-                if self.no_storage:
586-                    bw.throw_out_all_data = True
587-                bucketwriters[shnum] = bw
588-                self._active_writers[bw] = 1
589-                if limited:
590-                    remaining_space -= max_space_per_bucket
591-            else:
592-                # bummer! not enough space to accept this bucket
593-                pass
594-
595-        if bucketwriters:
596-            fileutil.make_dirs(os.path.join(self.sharedir, si_dir))
597 
598         self.add_latency("allocate", time.time() - start)
599         return alreadygot, bucketwriters
600hunk ./src/allmydata/storage/server.py 437
601             self.stats_provider.count('storage_server.bytes_added', consumed_size)
602         del self._active_writers[bw]
603 
604-    def _get_bucket_shares(self, storage_index):
605-        """Return a list of (shnum, pathname) tuples for files that hold
606-        shares for this storage_index. In each tuple, 'shnum' will always be
607-        the integer form of the last component of 'pathname'."""
608-        storagedir = os.path.join(self.sharedir, storage_index_to_dir(storage_index))
609-        try:
610-            for f in os.listdir(storagedir):
611-                if NUM_RE.match(f):
612-                    filename = os.path.join(storagedir, f)
613-                    yield (int(f), filename)
614-        except OSError:
615-            # Commonly caused by there being no buckets at all.
616-            pass
617 
618     def remote_get_buckets(self, storage_index):
619         start = time.time()
620hunk ./src/allmydata/storage/server.py 444
621         si_s = si_b2a(storage_index)
622         log.msg("storage: get_buckets %s" % si_s)
623         bucketreaders = {} # k: sharenum, v: BucketReader
624-        for shnum, filename in self._get_bucket_shares(storage_index):
625+        for shnum, filename in self.backend.get_bucket_shares(storage_index):
626             bucketreaders[shnum] = BucketReader(self, filename,
627                                                 storage_index, shnum)
628         self.add_latency("get", time.time() - start)
629hunk ./src/allmydata/test/test_backends.py 10
630 import mock
631 
632 # This is the code that we're going to be testing.
633-from allmydata.storage.server import StorageServer
634+from allmydata.storage.server import StorageServer, FSBackend, NullBackend
635 
636 # The following share file contents was generated with
637 # storage.immutable.ShareFile from Tahoe-LAFS v1.8.2
638hunk ./src/allmydata/test/test_backends.py 21
639 sharefname = 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a/0'
640 
641 class TestServerConstruction(unittest.TestCase, ReallyEqualMixin):
642+    @mock.patch('time.time')
643+    @mock.patch('os.mkdir')
644+    @mock.patch('__builtin__.open')
645+    @mock.patch('os.listdir')
646+    @mock.patch('os.path.isdir')
647+    def test_create_server_null_backend(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
648+        """ This tests whether a server instance can be constructed
649+        with a null backend. The server instance fails the test if it
650+        tries to read or write to the file system. """
651+
652+        # Now begin the test.
653+        s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
654+
655+        self.failIf(mockisdir.called)
656+        self.failIf(mocklistdir.called)
657+        self.failIf(mockopen.called)
658+        self.failIf(mockmkdir.called)
659+
660+        # You passed!
661+
662+    @mock.patch('time.time')
663+    @mock.patch('os.mkdir')
664     @mock.patch('__builtin__.open')
665hunk ./src/allmydata/test/test_backends.py 44
666-    def test_create_server(self, mockopen):
667-        """ This tests whether a server instance can be constructed. """
668+    @mock.patch('os.listdir')
669+    @mock.patch('os.path.isdir')
670+    def test_create_server_fs_backend(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
671+        """ This tests whether a server instance can be constructed
672+        with a filesystem backend. To pass the test, it has to use the
673+        filesystem in only the prescribed ways. """
674 
675         def call_open(fname, mode):
676             if fname == 'testdir/bucket_counter.state':
677hunk ./src/allmydata/test/test_backends.py 58
678                 raise IOError(2, "No such file or directory: 'testdir/lease_checker.state'")
679             elif fname == 'testdir/lease_checker.history':
680                 return StringIO()
681+            else:
682+                self.fail("Server with FS backend tried to open '%s' in mode '%s'" % (fname, mode))
683         mockopen.side_effect = call_open
684 
685         # Now begin the test.
686hunk ./src/allmydata/test/test_backends.py 63
687-        s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
688+        s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
689+
690+        self.failIf(mockisdir.called)
691+        self.failIf(mocklistdir.called)
692+        self.failIf(mockopen.called)
693+        self.failIf(mockmkdir.called)
694+        self.failIf(mocktime.called)
695 
696         # You passed!
697 
698hunk ./src/allmydata/test/test_backends.py 73
699-class TestServer(unittest.TestCase, ReallyEqualMixin):
700+class TestServerNullBackend(unittest.TestCase, ReallyEqualMixin):
701+    def setUp(self):
702+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=NullBackend())
703+
704+    @mock.patch('os.mkdir')
705+    @mock.patch('__builtin__.open')
706+    @mock.patch('os.listdir')
707+    @mock.patch('os.path.isdir')
708+    def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir):
709+        """ Write a new share. """
710+
711+        # Now begin the test.
712+        alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
713+        bs[0].remote_write(0, 'a')
714+        self.failIf(mockisdir.called)
715+        self.failIf(mocklistdir.called)
716+        self.failIf(mockopen.called)
717+        self.failIf(mockmkdir.called)
718+
719+    @mock.patch('os.path.exists')
720+    @mock.patch('os.path.getsize')
721+    @mock.patch('__builtin__.open')
722+    @mock.patch('os.listdir')
723+    def test_read_share(self, mocklistdir, mockopen, mockgetsize, mockexists):
724+        """ This tests whether the code correctly finds and reads
725+        shares written out by old (Tahoe-LAFS <= v1.8.2)
726+        servers. There is a similar test in test_download, but that one
727+        is from the perspective of the client and exercises a deeper
728+        stack of code. This one is for exercising just the
729+        StorageServer object. """
730+
731+        # Now begin the test.
732+        bs = self.s.remote_get_buckets('teststorage_index')
733+
734+        self.failUnlessEqual(len(bs), 0)
735+        self.failIf(mocklistdir.called)
736+        self.failIf(mockopen.called)
737+        self.failIf(mockgetsize.called)
738+        self.failIf(mockexists.called)
739+
740+
741+class TestServerFSBackend(unittest.TestCase, ReallyEqualMixin):
742     @mock.patch('__builtin__.open')
743     def setUp(self, mockopen):
744         def call_open(fname, mode):
745hunk ./src/allmydata/test/test_backends.py 126
746                 return StringIO()
747         mockopen.side_effect = call_open
748 
749-        self.s = StorageServer('testdir', 'testnodeidxxxxxxxxxx')
750-
751+        self.s = StorageServer('testnodeidxxxxxxxxxx', backend=FSBackend('teststoredir'))
752 
753     @mock.patch('time.time')
754     @mock.patch('os.mkdir')
755hunk ./src/allmydata/test/test_backends.py 134
756     @mock.patch('os.listdir')
757     @mock.patch('os.path.isdir')
758     def test_write_share(self, mockisdir, mocklistdir, mockopen, mockmkdir, mocktime):
759-        """Handle a report of corruption."""
760+        """ Write a new share. """
761 
762         def call_listdir(dirname):
763             self.failUnlessReallyEqual(dirname, 'testdir/shares/or/orsxg5dtorxxeylhmvpws3temv4a')
764hunk ./src/allmydata/test/test_backends.py 173
765         mockopen.side_effect = call_open
766         # Now begin the test.
767         alreadygot, bs = self.s.remote_allocate_buckets('teststorage_index', 'x'*32, 'y'*32, set((0,)), 1, mock.Mock())
768-        print bs
769         bs[0].remote_write(0, 'a')
770         self.failUnlessReallyEqual(sharefile.buffer, share_file_data)
771 
772hunk ./src/allmydata/test/test_backends.py 176
773-
774     @mock.patch('os.path.exists')
775     @mock.patch('os.path.getsize')
776     @mock.patch('__builtin__.open')
777hunk ./src/allmydata/test/test_backends.py 218
778 
779         self.failUnlessEqual(len(bs), 1)
780         b = bs[0]
781+        # These should match by definition, the next two cases cover cases without (completely) unambiguous behaviors.
782         self.failUnlessReallyEqual(b.remote_read(0, datalen), share_data)
783         # If you try to read past the end you get the as much data as is there.
784         self.failUnlessReallyEqual(b.remote_read(0, datalen+20), share_data)
785hunk ./src/allmydata/test/test_backends.py 224
786         # If you start reading past the end of the file you get the empty string.
787         self.failUnlessReallyEqual(b.remote_read(datalen+1, 3), '')
788+
789+
790}
791
792Context:
793
794[Rename test_package_initialization.py to (much shorter) test_import.py .
795Brian Warner <warner@lothar.com>**20110611190234
796 Ignore-this: 3eb3dbac73600eeff5cfa6b65d65822
797 
798 The former name was making my 'ls' listings hard to read, by forcing them
799 down to just two columns.
800]
801[tests: fix tests to accomodate [20110611153758-92b7f-0ba5e4726fb6318dac28fb762a6512a003f4c430]
802zooko@zooko.com**20110611163741
803 Ignore-this: 64073a5f39e7937e8e5e1314c1a302d1
804 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.
805 fixes #1412
806]
807[wui: right-align the size column in the WUI
808zooko@zooko.com**20110611153758
809 Ignore-this: 492bdaf4373c96f59f90581c7daf7cd7
810 Thanks to Ted "stercor" Rolle Jr. and Terrell Russell.
811 fixes #1412
812]
813[docs: three minor fixes
814zooko@zooko.com**20110610121656
815 Ignore-this: fec96579eb95aceb2ad5fc01a814c8a2
816 CREDITS for arc for stats tweak
817 fix link to .zip file in quickstart.rst (thanks to ChosenOne for noticing)
818 English usage tweak
819]
820[docs/running.rst: fix stray HTML (not .rst) link noticed by ChosenOne.
821david-sarah@jacaranda.org**20110609223719
822 Ignore-this: fc50ac9c94792dcac6f1067df8ac0d4a
823]
824[server.py:  get_latencies now reports percentiles _only_ if there are sufficient observations for the interpretation of the percentile to be unambiguous.
825wilcoxjg@gmail.com**20110527120135
826 Ignore-this: 2e7029764bffc60e26f471d7c2b6611e
827 interfaces.py:  modified the return type of RIStatsProvider.get_stats to allow for None as a return value
828 NEWS.rst, stats.py: documentation of change to get_latencies
829 stats.rst: now documents percentile modification in get_latencies
830 test_storage.py:  test_latencies now expects None in output categories that contain too few samples for the associated percentile to be unambiguously reported.
831 fixes #1392
832]
833[docs: revert link in relnotes.txt from NEWS.rst to NEWS, since the former did not exist at revision 5000.
834david-sarah@jacaranda.org**20110517011214
835 Ignore-this: 6a5be6e70241e3ec0575641f64343df7
836]
837[docs: convert NEWS to NEWS.rst and change all references to it.
838david-sarah@jacaranda.org**20110517010255
839 Ignore-this: a820b93ea10577c77e9c8206dbfe770d
840]
841[docs: remove out-of-date docs/testgrid/introducer.furl and containing directory. fixes #1404
842david-sarah@jacaranda.org**20110512140559
843 Ignore-this: 784548fc5367fac5450df1c46890876d
844]
845[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
846david-sarah@jacaranda.org**20110130164923
847 Ignore-this: a271e77ce81d84bb4c43645b891d92eb
848]
849[setup: don't catch all Exception from check_requirement(), but only PackagingError and ImportError
850zooko@zooko.com**20110128142006
851 Ignore-this: 57d4bc9298b711e4bc9dc832c75295de
852 I noticed this because I had accidentally inserted a bug which caused AssertionError to be raised from check_requirement().
853]
854[M-x whitespace-cleanup
855zooko@zooko.com**20110510193653
856 Ignore-this: dea02f831298c0f65ad096960e7df5c7
857]
858[docs: fix typo in running.rst, thanks to arch_o_median
859zooko@zooko.com**20110510193633
860 Ignore-this: ca06de166a46abbc61140513918e79e8
861]
862[relnotes.txt: don't claim to work on Cygwin (which has been untested for some time). refs #1342
863david-sarah@jacaranda.org**20110204204902
864 Ignore-this: 85ef118a48453d93fa4cddc32d65b25b
865]
866[relnotes.txt: forseeable -> foreseeable. refs #1342
867david-sarah@jacaranda.org**20110204204116
868 Ignore-this: 746debc4d82f4031ebf75ab4031b3a9
869]
870[replace remaining .html docs with .rst docs
871zooko@zooko.com**20110510191650
872 Ignore-this: d557d960a986d4ac8216d1677d236399
873 Remove install.html (long since deprecated).
874 Also replace some obsolete references to install.html with references to quickstart.rst.
875 Fix some broken internal references within docs/historical/historical_known_issues.txt.
876 Thanks to Ravi Pinjala and Patrick McDonald.
877 refs #1227
878]
879[docs: FTP-and-SFTP.rst: fix a minor error and update the information about which version of Twisted fixes #1297
880zooko@zooko.com**20110428055232
881 Ignore-this: b63cfb4ebdbe32fb3b5f885255db4d39
882]
883[munin tahoe_files plugin: fix incorrect file count
884francois@ctrlaltdel.ch**20110428055312
885 Ignore-this: 334ba49a0bbd93b4a7b06a25697aba34
886 fixes #1391
887]
888[corrected "k must never be smaller than N" to "k must never be greater than N"
889secorp@allmydata.org**20110425010308
890 Ignore-this: 233129505d6c70860087f22541805eac
891]
892[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
893david-sarah@jacaranda.org**20110411190738
894 Ignore-this: 7847d26bc117c328c679f08a7baee519
895]
896[tests: add test for including the ImportError message and traceback entry in the summary of errors from importing dependencies. refs #1389
897david-sarah@jacaranda.org**20110410155844
898 Ignore-this: fbecdbeb0d06a0f875fe8d4030aabafa
899]
900[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
901david-sarah@jacaranda.org**20110410155705
902 Ignore-this: 2f87b8b327906cf8bfca9440a0904900
903]
904[remove unused variable detected by pyflakes
905zooko@zooko.com**20110407172231
906 Ignore-this: 7344652d5e0720af822070d91f03daf9
907]
908[allmydata/__init__.py: Nicer reporting of unparseable version numbers in dependencies. fixes #1388
909david-sarah@jacaranda.org**20110401202750
910 Ignore-this: 9c6bd599259d2405e1caadbb3e0d8c7f
911]
912[update FTP-and-SFTP.rst: the necessary patch is included in Twisted-10.1
913Brian Warner <warner@lothar.com>**20110325232511
914 Ignore-this: d5307faa6900f143193bfbe14e0f01a
915]
916[control.py: remove all uses of s.get_serverid()
917warner@lothar.com**20110227011203
918 Ignore-this: f80a787953bd7fa3d40e828bde00e855
919]
920[web: remove some uses of s.get_serverid(), not all
921warner@lothar.com**20110227011159
922 Ignore-this: a9347d9cf6436537a47edc6efde9f8be
923]
924[immutable/downloader/fetcher.py: remove all get_serverid() calls
925warner@lothar.com**20110227011156
926 Ignore-this: fb5ef018ade1749348b546ec24f7f09a
927]
928[immutable/downloader/fetcher.py: fix diversity bug in server-response handling
929warner@lothar.com**20110227011153
930 Ignore-this: bcd62232c9159371ae8a16ff63d22c1b
931 
932 When blocks terminate (either COMPLETE or CORRUPT/DEAD/BADSEGNUM), the
933 _shares_from_server dict was being popped incorrectly (using shnum as the
934 index instead of serverid). I'm still thinking through the consequences of
935 this bug. It was probably benign and really hard to detect. I think it would
936 cause us to incorrectly believe that we're pulling too many shares from a
937 server, and thus prefer a different server rather than asking for a second
938 share from the first server. The diversity code is intended to spread out the
939 number of shares simultaneously being requested from each server, but with
940 this bug, it might be spreading out the total number of shares requested at
941 all, not just simultaneously. (note that SegmentFetcher is scoped to a single
942 segment, so the effect doesn't last very long).
943]
944[immutable/downloader/share.py: reduce get_serverid(), one left, update ext deps
945warner@lothar.com**20110227011150
946 Ignore-this: d8d56dd8e7b280792b40105e13664554
947 
948 test_download.py: create+check MyShare instances better, make sure they share
949 Server objects, now that finder.py cares
950]
951[immutable/downloader/finder.py: reduce use of get_serverid(), one left
952warner@lothar.com**20110227011146
953 Ignore-this: 5785be173b491ae8a78faf5142892020
954]
955[immutable/offloaded.py: reduce use of get_serverid() a bit more
956warner@lothar.com**20110227011142
957 Ignore-this: b48acc1b2ae1b311da7f3ba4ffba38f
958]
959[immutable/upload.py: reduce use of get_serverid()
960warner@lothar.com**20110227011138
961 Ignore-this: ffdd7ff32bca890782119a6e9f1495f6
962]
963[immutable/checker.py: remove some uses of s.get_serverid(), not all
964warner@lothar.com**20110227011134
965 Ignore-this: e480a37efa9e94e8016d826c492f626e
966]
967[add remaining get_* methods to storage_client.Server, NoNetworkServer, and
968warner@lothar.com**20110227011132
969 Ignore-this: 6078279ddf42b179996a4b53bee8c421
970 MockIServer stubs
971]
972[upload.py: rearrange _make_trackers a bit, no behavior changes
973warner@lothar.com**20110227011128
974 Ignore-this: 296d4819e2af452b107177aef6ebb40f
975]
976[happinessutil.py: finally rename merge_peers to merge_servers
977warner@lothar.com**20110227011124
978 Ignore-this: c8cd381fea1dd888899cb71e4f86de6e
979]
980[test_upload.py: factor out FakeServerTracker
981warner@lothar.com**20110227011120
982 Ignore-this: 6c182cba90e908221099472cc159325b
983]
984[test_upload.py: server-vs-tracker cleanup
985warner@lothar.com**20110227011115
986 Ignore-this: 2915133be1a3ba456e8603885437e03
987]
988[happinessutil.py: server-vs-tracker cleanup
989warner@lothar.com**20110227011111
990 Ignore-this: b856c84033562d7d718cae7cb01085a9
991]
992[upload.py: more tracker-vs-server cleanup
993warner@lothar.com**20110227011107
994 Ignore-this: bb75ed2afef55e47c085b35def2de315
995]
996[upload.py: fix var names to avoid confusion between 'trackers' and 'servers'
997warner@lothar.com**20110227011103
998 Ignore-this: 5d5e3415b7d2732d92f42413c25d205d
999]
1000[refactor: s/peer/server/ in immutable/upload, happinessutil.py, test_upload
1001warner@lothar.com**20110227011100
1002 Ignore-this: 7ea858755cbe5896ac212a925840fe68
1003 
1004 No behavioral changes, just updating variable/method names and log messages.
1005 The effects outside these three files should be minimal: some exception
1006 messages changed (to say "server" instead of "peer"), and some internal class
1007 names were changed. A few things still use "peer" to minimize external
1008 changes, like UploadResults.timings["peer_selection"] and
1009 happinessutil.merge_peers, which can be changed later.
1010]
1011[storage_client.py: clean up test_add_server/test_add_descriptor, remove .test_servers
1012warner@lothar.com**20110227011056
1013 Ignore-this: efad933e78179d3d5fdcd6d1ef2b19cc
1014]
1015[test_client.py, upload.py:: remove KiB/MiB/etc constants, and other dead code
1016warner@lothar.com**20110227011051
1017 Ignore-this: dc83c5794c2afc4f81e592f689c0dc2d
1018]
1019[test: increase timeout on a network test because Francois's ARM machine hit that timeout
1020zooko@zooko.com**20110317165909
1021 Ignore-this: 380c345cdcbd196268ca5b65664ac85b
1022 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.
1023]
1024[docs/configuration.rst: add a "Frontend Configuration" section
1025Brian Warner <warner@lothar.com>**20110222014323
1026 Ignore-this: 657018aa501fe4f0efef9851628444ca
1027 
1028 this points to docs/frontends/*.rst, which were previously underlinked
1029]
1030[web/filenode.py: avoid calling req.finish() on closed HTTP connections. Closes #1366
1031"Brian Warner <warner@lothar.com>"**20110221061544
1032 Ignore-this: 799d4de19933f2309b3c0c19a63bb888
1033]
1034[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.
1035david-sarah@jacaranda.org**20110221015817
1036 Ignore-this: 51d181698f8c20d3aca58b057e9c475a
1037]
1038[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.
1039david-sarah@jacaranda.org**20110221020125
1040 Ignore-this: b0744ed58f161bf188e037bad077fc48
1041]
1042[Refactor StorageFarmBroker handling of servers
1043Brian Warner <warner@lothar.com>**20110221015804
1044 Ignore-this: 842144ed92f5717699b8f580eab32a51
1045 
1046 Pass around IServer instance instead of (peerid, rref) tuple. Replace
1047 "descriptor" with "server". Other replacements:
1048 
1049  get_all_servers -> get_connected_servers/get_known_servers
1050  get_servers_for_index -> get_servers_for_psi (now returns IServers)
1051 
1052 This change still needs to be pushed further down: lots of code is now
1053 getting the IServer and then distributing (peerid, rref) internally.
1054 Instead, it ought to distribute the IServer internally and delay
1055 extracting a serverid or rref until the last moment.
1056 
1057 no_network.py was updated to retain parallelism.
1058]
1059[TAG allmydata-tahoe-1.8.2
1060warner@lothar.com**20110131020101]
1061Patch bundle hash:
1062f5f17113e7ee758a831726c346edff9b6ed62c2a