# Copyright 2017 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. import contextlib import errno import common from autotest_lib.server.hosts import host_info from chromite.lib import locking from chromite.lib import retry_util _FILE_LOCK_TIMEOUT_SECONDS = 5 class FileStore(host_info.CachingHostInfoStore): """A CachingHostInfoStore backed by an on-disk file.""" def __init__(self, store_file, file_lock_timeout_seconds=_FILE_LOCK_TIMEOUT_SECONDS): """ @param store_file: Absolute path to the backing file to use. @param info: Optional HostInfo to initialize the store. When not None, any data in store_file will be overwritten. @param file_lock_timeout_seconds: Timeout for aborting the attempt to lock the backing file in seconds. Set this to <= 0 to request just a single attempt. """ super(FileStore, self).__init__() self._store_file = store_file self._lock_path = '%s.lock' % store_file if file_lock_timeout_seconds <= 0: self._lock_max_retry = 0 self._lock_sleep = 0 else: # A total of 3 attempts at times (0 + sleep + 2*sleep). self._lock_max_retry = 2 self._lock_sleep = file_lock_timeout_seconds / 3.0 self._lock = locking.FileLock( self._lock_path, locktype=locking.FLOCK, description='Locking FileStore to read/write HostInfo.', blocking=False) def __str__(self): return '%s[%s]' % (type(self).__name__, self._store_file) def _refresh_impl(self): """See parent class docstring.""" with self._lock_backing_file(): return self._refresh_impl_locked() def _commit_impl(self, info): """See parent class docstring.""" with self._lock_backing_file(): return self._commit_impl_locked(info) def _refresh_impl_locked(self): """Same as _refresh_impl, but assumes relevant files are locked.""" try: with open(self._store_file, 'r') as fp: return host_info.json_deserialize(fp) except IOError as e: if e.errno == errno.ENOENT: raise host_info.StoreError( 'No backing file. You must commit to the store before ' 'trying to read a value from it.') raise host_info.StoreError('Failed to read backing file (%s) : %r' % (self._store_file, e)) except host_info.DeserializationError as e: raise host_info.StoreError( 'Failed to desrialize backing file %s: %r' % (self._store_file, e)) def _commit_impl_locked(self, info): """Same as _commit_impl, but assumes relevant files are locked.""" try: with open(self._store_file, 'w') as fp: host_info.json_serialize(info, fp) except IOError as e: raise host_info.StoreError('Failed to write backing file (%s) : %r' % (self._store_file, e)) @contextlib.contextmanager def _lock_backing_file(self): """Context to lock the backing store file. @raises StoreError if the backing file can not be locked. """ def _retry_locking_failures(exc): return isinstance(exc, locking.LockNotAcquiredError) try: retry_util.GenericRetry( handler=_retry_locking_failures, functor=self._lock.write_lock, max_retry=self._lock_max_retry, sleep=self._lock_sleep) # If self._lock fails to write the locking file, it'll leak an OSError except (locking.LockNotAcquiredError, OSError) as e: raise host_info.StoreError(e) with self._lock: yield