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