xref: /aosp_15_r20/external/autotest/server/hosts/file_store.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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