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