1# Copyright 2017 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5import contextlib 6import errno 7 8import common 9from autotest_lib.server.hosts import host_info 10from chromite.lib import locking 11from chromite.lib import retry_util 12 13 14_FILE_LOCK_TIMEOUT_SECONDS = 5 15 16 17class FileStore(host_info.CachingHostInfoStore): 18 """A CachingHostInfoStore backed by an on-disk file.""" 19 20 def __init__(self, store_file, 21 file_lock_timeout_seconds=_FILE_LOCK_TIMEOUT_SECONDS): 22 """ 23 @param store_file: Absolute path to the backing file to use. 24 @param info: Optional HostInfo to initialize the store. When not None, 25 any data in store_file will be overwritten. 26 @param file_lock_timeout_seconds: Timeout for aborting the attempt to 27 lock the backing file in seconds. Set this to <= 0 to request 28 just a single attempt. 29 """ 30 super(FileStore, self).__init__() 31 self._store_file = store_file 32 self._lock_path = '%s.lock' % store_file 33 34 if file_lock_timeout_seconds <= 0: 35 self._lock_max_retry = 0 36 self._lock_sleep = 0 37 else: 38 # A total of 3 attempts at times (0 + sleep + 2*sleep). 39 self._lock_max_retry = 2 40 self._lock_sleep = file_lock_timeout_seconds / 3.0 41 self._lock = locking.FileLock( 42 self._lock_path, 43 locktype=locking.FLOCK, 44 description='Locking FileStore to read/write HostInfo.', 45 blocking=False) 46 47 48 def __str__(self): 49 return '%s[%s]' % (type(self).__name__, self._store_file) 50 51 52 def _refresh_impl(self): 53 """See parent class docstring.""" 54 with self._lock_backing_file(): 55 return self._refresh_impl_locked() 56 57 58 def _commit_impl(self, info): 59 """See parent class docstring.""" 60 with self._lock_backing_file(): 61 return self._commit_impl_locked(info) 62 63 64 def _refresh_impl_locked(self): 65 """Same as _refresh_impl, but assumes relevant files are locked.""" 66 try: 67 with open(self._store_file, 'r') as fp: 68 return host_info.json_deserialize(fp) 69 except IOError as e: 70 if e.errno == errno.ENOENT: 71 raise host_info.StoreError( 72 'No backing file. You must commit to the store before ' 73 'trying to read a value from it.') 74 raise host_info.StoreError('Failed to read backing file (%s) : %r' 75 % (self._store_file, e)) 76 except host_info.DeserializationError as e: 77 raise host_info.StoreError( 78 'Failed to desrialize backing file %s: %r' % 79 (self._store_file, e)) 80 81 82 def _commit_impl_locked(self, info): 83 """Same as _commit_impl, but assumes relevant files are locked.""" 84 try: 85 with open(self._store_file, 'w') as fp: 86 host_info.json_serialize(info, fp) 87 except IOError as e: 88 raise host_info.StoreError('Failed to write backing file (%s) : %r' 89 % (self._store_file, e)) 90 91 92 @contextlib.contextmanager 93 def _lock_backing_file(self): 94 """Context to lock the backing store file. 95 96 @raises StoreError if the backing file can not be locked. 97 """ 98 def _retry_locking_failures(exc): 99 return isinstance(exc, locking.LockNotAcquiredError) 100 101 try: 102 retry_util.GenericRetry( 103 handler=_retry_locking_failures, 104 functor=self._lock.write_lock, 105 max_retry=self._lock_max_retry, 106 sleep=self._lock_sleep) 107 # If self._lock fails to write the locking file, it'll leak an OSError 108 except (locking.LockNotAcquiredError, OSError) as e: 109 raise host_info.StoreError(e) 110 111 with self._lock: 112 yield 113