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