• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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