OLD | NEW |
(Empty) | |
| 1 """ |
| 2 File Backends |
| 3 ------------------ |
| 4 |
| 5 Provides backends that deal with local filesystem access. |
| 6 |
| 7 """ |
| 8 |
| 9 from __future__ import with_statement |
| 10 from ..api import CacheBackend, NO_VALUE |
| 11 from contextlib import contextmanager |
| 12 from ...util import compat |
| 13 from ... import util |
| 14 import os |
| 15 |
| 16 __all__ = 'DBMBackend', 'FileLock', 'AbstractFileLock' |
| 17 |
| 18 |
| 19 class DBMBackend(CacheBackend): |
| 20 """A file-backend using a dbm file to store keys. |
| 21 |
| 22 Basic usage:: |
| 23 |
| 24 from dogpile.cache import make_region |
| 25 |
| 26 region = make_region().configure( |
| 27 'dogpile.cache.dbm', |
| 28 expiration_time = 3600, |
| 29 arguments = { |
| 30 "filename":"/path/to/cachefile.dbm" |
| 31 } |
| 32 ) |
| 33 |
| 34 DBM access is provided using the Python ``anydbm`` module, |
| 35 which selects a platform-specific dbm module to use. |
| 36 This may be made to be more configurable in a future |
| 37 release. |
| 38 |
| 39 Note that different dbm modules have different behaviors. |
| 40 Some dbm implementations handle their own locking, while |
| 41 others don't. The :class:`.DBMBackend` uses a read/write |
| 42 lockfile by default, which is compatible even with those |
| 43 DBM implementations for which this is unnecessary, |
| 44 though the behavior can be disabled. |
| 45 |
| 46 The DBM backend by default makes use of two lockfiles. |
| 47 One is in order to protect the DBM file itself from |
| 48 concurrent writes, the other is to coordinate |
| 49 value creation (i.e. the dogpile lock). By default, |
| 50 these lockfiles use the ``flock()`` system call |
| 51 for locking; this is **only available on Unix |
| 52 platforms**. An alternative lock implementation, such as one |
| 53 which is based on threads or uses a third-party system |
| 54 such as `portalocker <https://pypi.python.org/pypi/portalocker>`_, |
| 55 can be dropped in using the ``lock_factory`` argument |
| 56 in conjunction with the :class:`.AbstractFileLock` base class. |
| 57 |
| 58 Currently, the dogpile lock is against the entire |
| 59 DBM file, not per key. This means there can |
| 60 only be one "creator" job running at a time |
| 61 per dbm file. |
| 62 |
| 63 A future improvement might be to have the dogpile lock |
| 64 using a filename that's based on a modulus of the key. |
| 65 Locking on a filename that uniquely corresponds to the |
| 66 key is problematic, since it's not generally safe to |
| 67 delete lockfiles as the application runs, implying an |
| 68 unlimited number of key-based files would need to be |
| 69 created and never deleted. |
| 70 |
| 71 Parameters to the ``arguments`` dictionary are |
| 72 below. |
| 73 |
| 74 :param filename: path of the filename in which to |
| 75 create the DBM file. Note that some dbm backends |
| 76 will change this name to have additional suffixes. |
| 77 :param rw_lockfile: the name of the file to use for |
| 78 read/write locking. If omitted, a default name |
| 79 is used by appending the suffix ".rw.lock" to the |
| 80 DBM filename. If False, then no lock is used. |
| 81 :param dogpile_lockfile: the name of the file to use |
| 82 for value creation, i.e. the dogpile lock. If |
| 83 omitted, a default name is used by appending the |
| 84 suffix ".dogpile.lock" to the DBM filename. If |
| 85 False, then dogpile.cache uses the default dogpile |
| 86 lock, a plain thread-based mutex. |
| 87 :param lock_factory: a function or class which provides |
| 88 for a read/write lock. Defaults to :class:`.FileLock`. |
| 89 Custom implementations need to implement context-manager |
| 90 based ``read()`` and ``write()`` functions - the |
| 91 :class:`.AbstractFileLock` class is provided as a base class |
| 92 which provides these methods based on individual read/write lock |
| 93 functions. E.g. to replace the lock with the dogpile.core |
| 94 :class:`.ReadWriteMutex`:: |
| 95 |
| 96 from dogpile.core.readwrite_lock import ReadWriteMutex |
| 97 from dogpile.cache.backends.file import AbstractFileLock |
| 98 |
| 99 class MutexLock(AbstractFileLock): |
| 100 def __init__(self, filename): |
| 101 self.mutex = ReadWriteMutex() |
| 102 |
| 103 def acquire_read_lock(self, wait): |
| 104 ret = self.mutex.acquire_read_lock(wait) |
| 105 return wait or ret |
| 106 |
| 107 def acquire_write_lock(self, wait): |
| 108 ret = self.mutex.acquire_write_lock(wait) |
| 109 return wait or ret |
| 110 |
| 111 def release_read_lock(self): |
| 112 return self.mutex.release_read_lock() |
| 113 |
| 114 def release_write_lock(self): |
| 115 return self.mutex.release_write_lock() |
| 116 |
| 117 from dogpile.cache import make_region |
| 118 |
| 119 region = make_region().configure( |
| 120 "dogpile.cache.dbm", |
| 121 expiration_time=300, |
| 122 arguments={ |
| 123 "filename": "file.dbm", |
| 124 "lock_factory": MutexLock |
| 125 } |
| 126 ) |
| 127 |
| 128 While the included :class:`.FileLock` uses ``os.flock()``, a |
| 129 windows-compatible implementation can be built using a library |
| 130 such as `portalocker <https://pypi.python.org/pypi/portalocker>`_. |
| 131 |
| 132 .. versionadded:: 0.5.2 |
| 133 |
| 134 |
| 135 |
| 136 """ |
| 137 def __init__(self, arguments): |
| 138 self.filename = os.path.abspath( |
| 139 os.path.normpath(arguments['filename']) |
| 140 ) |
| 141 dir_, filename = os.path.split(self.filename) |
| 142 |
| 143 self.lock_factory = arguments.get("lock_factory", FileLock) |
| 144 self._rw_lock = self._init_lock( |
| 145 arguments.get('rw_lockfile'), |
| 146 ".rw.lock", dir_, filename) |
| 147 self._dogpile_lock = self._init_lock( |
| 148 arguments.get('dogpile_lockfile'), |
| 149 ".dogpile.lock", |
| 150 dir_, filename, |
| 151 util.KeyReentrantMutex.factory) |
| 152 |
| 153 # TODO: make this configurable |
| 154 if compat.py3k: |
| 155 import dbm |
| 156 else: |
| 157 import anydbm as dbm |
| 158 self.dbmmodule = dbm |
| 159 self._init_dbm_file() |
| 160 |
| 161 def _init_lock(self, argument, suffix, basedir, basefile, wrapper=None): |
| 162 if argument is None: |
| 163 lock = self.lock_factory(os.path.join(basedir, basefile + suffix)) |
| 164 elif argument is not False: |
| 165 lock = self.lock_factory( |
| 166 os.path.abspath( |
| 167 os.path.normpath(argument) |
| 168 )) |
| 169 else: |
| 170 return None |
| 171 if wrapper: |
| 172 lock = wrapper(lock) |
| 173 return lock |
| 174 |
| 175 def _init_dbm_file(self): |
| 176 exists = os.access(self.filename, os.F_OK) |
| 177 if not exists: |
| 178 for ext in ('db', 'dat', 'pag', 'dir'): |
| 179 if os.access(self.filename + os.extsep + ext, os.F_OK): |
| 180 exists = True |
| 181 break |
| 182 if not exists: |
| 183 fh = self.dbmmodule.open(self.filename, 'c') |
| 184 fh.close() |
| 185 |
| 186 def get_mutex(self, key): |
| 187 # using one dogpile for the whole file. Other ways |
| 188 # to do this might be using a set of files keyed to a |
| 189 # hash/modulus of the key. the issue is it's never |
| 190 # really safe to delete a lockfile as this can |
| 191 # break other processes trying to get at the file |
| 192 # at the same time - so handling unlimited keys |
| 193 # can't imply unlimited filenames |
| 194 if self._dogpile_lock: |
| 195 return self._dogpile_lock(key) |
| 196 else: |
| 197 return None |
| 198 |
| 199 @contextmanager |
| 200 def _use_rw_lock(self, write): |
| 201 if self._rw_lock is None: |
| 202 yield |
| 203 elif write: |
| 204 with self._rw_lock.write(): |
| 205 yield |
| 206 else: |
| 207 with self._rw_lock.read(): |
| 208 yield |
| 209 |
| 210 @contextmanager |
| 211 def _dbm_file(self, write): |
| 212 with self._use_rw_lock(write): |
| 213 dbm = self.dbmmodule.open( |
| 214 self.filename, |
| 215 "w" if write else "r") |
| 216 yield dbm |
| 217 dbm.close() |
| 218 |
| 219 def get(self, key): |
| 220 with self._dbm_file(False) as dbm: |
| 221 if hasattr(dbm, 'get'): |
| 222 value = dbm.get(key, NO_VALUE) |
| 223 else: |
| 224 # gdbm objects lack a .get method |
| 225 try: |
| 226 value = dbm[key] |
| 227 except KeyError: |
| 228 value = NO_VALUE |
| 229 if value is not NO_VALUE: |
| 230 value = compat.pickle.loads(value) |
| 231 return value |
| 232 |
| 233 def get_multi(self, keys): |
| 234 return [self.get(key) for key in keys] |
| 235 |
| 236 def set(self, key, value): |
| 237 with self._dbm_file(True) as dbm: |
| 238 dbm[key] = compat.pickle.dumps(value, |
| 239 compat.pickle.HIGHEST_PROTOCOL) |
| 240 |
| 241 def set_multi(self, mapping): |
| 242 with self._dbm_file(True) as dbm: |
| 243 for key, value in mapping.items(): |
| 244 dbm[key] = compat.pickle.dumps(value, |
| 245 compat.pickle.HIGHEST_PROTOCOL) |
| 246 |
| 247 def delete(self, key): |
| 248 with self._dbm_file(True) as dbm: |
| 249 try: |
| 250 del dbm[key] |
| 251 except KeyError: |
| 252 pass |
| 253 |
| 254 def delete_multi(self, keys): |
| 255 with self._dbm_file(True) as dbm: |
| 256 for key in keys: |
| 257 try: |
| 258 del dbm[key] |
| 259 except KeyError: |
| 260 pass |
| 261 |
| 262 |
| 263 class AbstractFileLock(object): |
| 264 """Coordinate read/write access to a file. |
| 265 |
| 266 typically is a file-based lock but doesn't necessarily have to be. |
| 267 |
| 268 The default implementation here is :class:`.FileLock`. |
| 269 |
| 270 Implementations should provide the following methods:: |
| 271 |
| 272 * __init__() |
| 273 * acquire_read_lock() |
| 274 * acquire_write_lock() |
| 275 * release_read_lock() |
| 276 * release_write_lock() |
| 277 |
| 278 The ``__init__()`` method accepts a single argument "filename", which |
| 279 may be used as the "lock file", for those implementations that use a lock |
| 280 file. |
| 281 |
| 282 Note that multithreaded environments must provide a thread-safe |
| 283 version of this lock. The recommended approach for file- |
| 284 descriptor-based locks is to use a Python ``threading.local()`` so |
| 285 that a unique file descriptor is held per thread. See the source |
| 286 code of :class:`.FileLock` for an implementation example. |
| 287 |
| 288 |
| 289 """ |
| 290 |
| 291 def __init__(self, filename): |
| 292 """Constructor, is given the filename of a potential lockfile. |
| 293 |
| 294 The usage of this filename is optional and no file is |
| 295 created by default. |
| 296 |
| 297 Raises ``NotImplementedError`` by default, must be |
| 298 implemented by subclasses. |
| 299 """ |
| 300 raise NotImplementedError() |
| 301 |
| 302 def acquire(self, wait=True): |
| 303 """Acquire the "write" lock. |
| 304 |
| 305 This is a direct call to :meth:`.AbstractFileLock.acquire_write_lock`. |
| 306 |
| 307 """ |
| 308 return self.acquire_write_lock(wait) |
| 309 |
| 310 def release(self): |
| 311 """Release the "write" lock. |
| 312 |
| 313 This is a direct call to :meth:`.AbstractFileLock.release_write_lock`. |
| 314 |
| 315 """ |
| 316 self.release_write_lock() |
| 317 |
| 318 @contextmanager |
| 319 def read(self): |
| 320 """Provide a context manager for the "read" lock. |
| 321 |
| 322 This method makes use of :meth:`.AbstractFileLock.acquire_read_lock` |
| 323 and :meth:`.AbstractFileLock.release_read_lock` |
| 324 |
| 325 """ |
| 326 |
| 327 self.acquire_read_lock(True) |
| 328 try: |
| 329 yield |
| 330 finally: |
| 331 self.release_read_lock() |
| 332 |
| 333 @contextmanager |
| 334 def write(self): |
| 335 """Provide a context manager for the "write" lock. |
| 336 |
| 337 This method makes use of :meth:`.AbstractFileLock.acquire_write_lock` |
| 338 and :meth:`.AbstractFileLock.release_write_lock` |
| 339 |
| 340 """ |
| 341 |
| 342 self.acquire_write_lock(True) |
| 343 try: |
| 344 yield |
| 345 finally: |
| 346 self.release_write_lock() |
| 347 |
| 348 @property |
| 349 def is_open(self): |
| 350 """optional method.""" |
| 351 raise NotImplementedError() |
| 352 |
| 353 def acquire_read_lock(self, wait): |
| 354 """Acquire a 'reader' lock. |
| 355 |
| 356 Raises ``NotImplementedError`` by default, must be |
| 357 implemented by subclasses. |
| 358 """ |
| 359 raise NotImplementedError() |
| 360 |
| 361 def acquire_write_lock(self, wait): |
| 362 """Acquire a 'write' lock. |
| 363 |
| 364 Raises ``NotImplementedError`` by default, must be |
| 365 implemented by subclasses. |
| 366 """ |
| 367 raise NotImplementedError() |
| 368 |
| 369 def release_read_lock(self): |
| 370 """Release a 'reader' lock. |
| 371 |
| 372 Raises ``NotImplementedError`` by default, must be |
| 373 implemented by subclasses. |
| 374 """ |
| 375 raise NotImplementedError() |
| 376 |
| 377 def release_write_lock(self): |
| 378 """Release a 'writer' lock. |
| 379 |
| 380 Raises ``NotImplementedError`` by default, must be |
| 381 implemented by subclasses. |
| 382 """ |
| 383 raise NotImplementedError() |
| 384 |
| 385 |
| 386 class FileLock(AbstractFileLock): |
| 387 """Use lockfiles to coordinate read/write access to a file. |
| 388 |
| 389 Only works on Unix systems, using |
| 390 `fcntl.flock() <http://docs.python.org/library/fcntl.html>`_. |
| 391 |
| 392 """ |
| 393 |
| 394 def __init__(self, filename): |
| 395 self._filedescriptor = compat.threading.local() |
| 396 self.filename = filename |
| 397 |
| 398 @util.memoized_property |
| 399 def _module(self): |
| 400 import fcntl |
| 401 return fcntl |
| 402 |
| 403 @property |
| 404 def is_open(self): |
| 405 return hasattr(self._filedescriptor, 'fileno') |
| 406 |
| 407 def acquire_read_lock(self, wait): |
| 408 return self._acquire(wait, os.O_RDONLY, self._module.LOCK_SH) |
| 409 |
| 410 def acquire_write_lock(self, wait): |
| 411 return self._acquire(wait, os.O_WRONLY, self._module.LOCK_EX) |
| 412 |
| 413 def release_read_lock(self): |
| 414 self._release() |
| 415 |
| 416 def release_write_lock(self): |
| 417 self._release() |
| 418 |
| 419 def _acquire(self, wait, wrflag, lockflag): |
| 420 wrflag |= os.O_CREAT |
| 421 fileno = os.open(self.filename, wrflag) |
| 422 try: |
| 423 if not wait: |
| 424 lockflag |= self._module.LOCK_NB |
| 425 self._module.flock(fileno, lockflag) |
| 426 except IOError: |
| 427 os.close(fileno) |
| 428 if not wait: |
| 429 # this is typically |
| 430 # "[Errno 35] Resource temporarily unavailable", |
| 431 # because of LOCK_NB |
| 432 return False |
| 433 else: |
| 434 raise |
| 435 else: |
| 436 self._filedescriptor.fileno = fileno |
| 437 return True |
| 438 |
| 439 def _release(self): |
| 440 try: |
| 441 fileno = self._filedescriptor.fileno |
| 442 except AttributeError: |
| 443 return |
| 444 else: |
| 445 self._module.flock(fileno, self._module.LOCK_UN) |
| 446 os.close(fileno) |
| 447 del self._filedescriptor.fileno |
OLD | NEW |