xref: /aosp_15_r20/external/autotest/server/hosts/shadowing_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 Lifrom __future__ import absolute_import
6*9c5db199SXin Lifrom __future__ import print_function
7*9c5db199SXin Li
8*9c5db199SXin Liimport logging
9*9c5db199SXin Li
10*9c5db199SXin Liimport common
11*9c5db199SXin Lifrom autotest_lib.server.hosts import host_info
12*9c5db199SXin Lifrom autotest_lib.utils.frozen_chromite.lib import metrics
13*9c5db199SXin Li
14*9c5db199SXin Li
15*9c5db199SXin Li_METRICS_PREFIX = 'chromeos/autotest/autoserv/host_info/shadowing_store/'
16*9c5db199SXin Li_REFRESH_METRIC_NAME = _METRICS_PREFIX + 'refresh_count'
17*9c5db199SXin Li_COMMIT_METRIC_NAME = _METRICS_PREFIX + 'commit_count'
18*9c5db199SXin Li
19*9c5db199SXin Li
20*9c5db199SXin Lilogger = logging.getLogger(__file__)
21*9c5db199SXin Li
22*9c5db199SXin Liclass ShadowingStore(host_info.CachingHostInfoStore):
23*9c5db199SXin Li    """A composite CachingHostInfoStore that maintains a main and shadow store.
24*9c5db199SXin Li
25*9c5db199SXin Li    ShadowingStore accepts two CachingHostInfoStore objects - primary_store and
26*9c5db199SXin Li    shadow_store. All refresh/commit operations are serviced through
27*9c5db199SXin Li    primary_store.  In addition, shadow_store is updated and compared with this
28*9c5db199SXin Li    information, leaving breadcrumbs when the two differ. Any errors in
29*9c5db199SXin Li    shadow_store operations are logged and ignored so as to not affect the user.
30*9c5db199SXin Li
31*9c5db199SXin Li    This is a transitional CachingHostInfoStore that allows us to continue to
32*9c5db199SXin Li    use an AfeStore in practice, but also create a backing FileStore so that we
33*9c5db199SXin Li    can validate the use of FileStore in prod.
34*9c5db199SXin Li    """
35*9c5db199SXin Li
36*9c5db199SXin Li    def __init__(self, primary_store, shadow_store,
37*9c5db199SXin Li                 mismatch_callback=None):
38*9c5db199SXin Li        """
39*9c5db199SXin Li        @param primary_store: A CachingHostInfoStore to be used as the primary
40*9c5db199SXin Li                store.
41*9c5db199SXin Li        @param shadow_store: A CachingHostInfoStore to be used to shadow the
42*9c5db199SXin Li                primary store.
43*9c5db199SXin Li        @param mismatch_callback: A callback used to notify whenever we notice a
44*9c5db199SXin Li                mismatch between primary_store and shadow_store. The signature
45*9c5db199SXin Li                of the callback must match:
46*9c5db199SXin Li                    callback(primary_info, shadow_info)
47*9c5db199SXin Li                where primary_info and shadow_info are HostInfo objects obtained
48*9c5db199SXin Li                from the two stores respectively.
49*9c5db199SXin Li                Mostly used by unittests. Actual users don't know / nor care
50*9c5db199SXin Li                that they're using a ShadowingStore.
51*9c5db199SXin Li        """
52*9c5db199SXin Li        super(ShadowingStore, self).__init__()
53*9c5db199SXin Li        self._primary_store = primary_store
54*9c5db199SXin Li        self._shadow_store = shadow_store
55*9c5db199SXin Li        self._mismatch_callback = (
56*9c5db199SXin Li                mismatch_callback if mismatch_callback is not None
57*9c5db199SXin Li                else _log_info_mismatch)
58*9c5db199SXin Li        try:
59*9c5db199SXin Li            self._shadow_store.commit(self._primary_store.get())
60*9c5db199SXin Li        except host_info.StoreError as e:
61*9c5db199SXin Li            metrics.Counter(
62*9c5db199SXin Li                    _METRICS_PREFIX + 'initialization_fail_count').increment()
63*9c5db199SXin Li            logger.exception(
64*9c5db199SXin Li                    'Failed to initialize shadow store. '
65*9c5db199SXin Li                    'Expect primary / shadow desync in the future.')
66*9c5db199SXin Li
67*9c5db199SXin Li    def commit_with_substitute(self, info, primary_store=None,
68*9c5db199SXin Li                               shadow_store=None):
69*9c5db199SXin Li        """Commit host information using alternative stores.
70*9c5db199SXin Li
71*9c5db199SXin Li        This is used to commit using an alternative store implementation
72*9c5db199SXin Li        to work around some issues (crbug.com/903589).
73*9c5db199SXin Li
74*9c5db199SXin Li        Don't set cached_info in this function.
75*9c5db199SXin Li
76*9c5db199SXin Li        @param info: A HostInfo object to set.
77*9c5db199SXin Li        @param primary_store: A CachingHostInfoStore object to commit instead of
78*9c5db199SXin Li            the original primary_store.
79*9c5db199SXin Li        @param shadow_store: A CachingHostInfoStore object to commit instead of
80*9c5db199SXin Li            the original shadow store.
81*9c5db199SXin Li        """
82*9c5db199SXin Li        if primary_store is not None:
83*9c5db199SXin Li            primary_store.commit(info)
84*9c5db199SXin Li        else:
85*9c5db199SXin Li            self._commit_to_primary_store(info)
86*9c5db199SXin Li
87*9c5db199SXin Li        if shadow_store is not None:
88*9c5db199SXin Li            shadow_store.commit(info)
89*9c5db199SXin Li        else:
90*9c5db199SXin Li            self._commit_to_shadow_store(info)
91*9c5db199SXin Li
92*9c5db199SXin Li    def __str__(self):
93*9c5db199SXin Li        return '%s[%s, %s]' % (type(self).__name__, self._primary_store,
94*9c5db199SXin Li                               self._shadow_store)
95*9c5db199SXin Li
96*9c5db199SXin Li    def _refresh_impl(self):
97*9c5db199SXin Li        """Obtains HostInfo from the primary and compares against shadow"""
98*9c5db199SXin Li        primary_info = self._refresh_from_primary_store()
99*9c5db199SXin Li        try:
100*9c5db199SXin Li            shadow_info = self._refresh_from_shadow_store()
101*9c5db199SXin Li        except host_info.StoreError:
102*9c5db199SXin Li            logger.exception('Shadow refresh failed. '
103*9c5db199SXin Li                             'Skipping comparison with primary.')
104*9c5db199SXin Li            return primary_info
105*9c5db199SXin Li        self._verify_store_infos(primary_info, shadow_info)
106*9c5db199SXin Li        return primary_info
107*9c5db199SXin Li
108*9c5db199SXin Li    def _commit_impl(self, info):
109*9c5db199SXin Li        """Commits HostInfo to both the primary and shadow store"""
110*9c5db199SXin Li        self._commit_to_primary_store(info)
111*9c5db199SXin Li        self._commit_to_shadow_store(info)
112*9c5db199SXin Li
113*9c5db199SXin Li    def _commit_to_primary_store(self, info):
114*9c5db199SXin Li        try:
115*9c5db199SXin Li            self._primary_store.commit(info)
116*9c5db199SXin Li        except host_info.StoreError:
117*9c5db199SXin Li            metrics.Counter(_COMMIT_METRIC_NAME).increment(
118*9c5db199SXin Li                    fields={'file_commit_result': 'skipped'})
119*9c5db199SXin Li            raise
120*9c5db199SXin Li
121*9c5db199SXin Li    def _commit_to_shadow_store(self, info):
122*9c5db199SXin Li        try:
123*9c5db199SXin Li            self._shadow_store.commit(info)
124*9c5db199SXin Li        except host_info.StoreError:
125*9c5db199SXin Li            logger.exception(
126*9c5db199SXin Li                    'shadow commit failed. '
127*9c5db199SXin Li                    'Expect primary / shadow desync in the future.')
128*9c5db199SXin Li            metrics.Counter(_COMMIT_METRIC_NAME).increment(
129*9c5db199SXin Li                    fields={'file_commit_result': 'fail'})
130*9c5db199SXin Li        else:
131*9c5db199SXin Li            metrics.Counter(_COMMIT_METRIC_NAME).increment(
132*9c5db199SXin Li                    fields={'file_commit_result': 'success'})
133*9c5db199SXin Li
134*9c5db199SXin Li    def _refresh_from_primary_store(self):
135*9c5db199SXin Li        try:
136*9c5db199SXin Li            return self._primary_store.get(force_refresh=True)
137*9c5db199SXin Li        except host_info.StoreError:
138*9c5db199SXin Li            metrics.Counter(_REFRESH_METRIC_NAME).increment(
139*9c5db199SXin Li                    fields={'validation_result': 'skipped'})
140*9c5db199SXin Li            raise
141*9c5db199SXin Li
142*9c5db199SXin Li    def _refresh_from_shadow_store(self):
143*9c5db199SXin Li        try:
144*9c5db199SXin Li            return self._shadow_store.get(force_refresh=True)
145*9c5db199SXin Li        except host_info.StoreError:
146*9c5db199SXin Li            metrics.Counter(_REFRESH_METRIC_NAME).increment(fields={
147*9c5db199SXin Li                    'validation_result': 'fail_shadow_store_refresh'})
148*9c5db199SXin Li            raise
149*9c5db199SXin Li
150*9c5db199SXin Li    def _verify_store_infos(self, primary_info, shadow_info):
151*9c5db199SXin Li        if primary_info == shadow_info:
152*9c5db199SXin Li            metrics.Counter(_REFRESH_METRIC_NAME).increment(
153*9c5db199SXin Li                    fields={'validation_result': 'success'})
154*9c5db199SXin Li        else:
155*9c5db199SXin Li            self._mismatch_callback(primary_info, shadow_info)
156*9c5db199SXin Li            metrics.Counter(_REFRESH_METRIC_NAME).increment(
157*9c5db199SXin Li                    fields={'validation_result': 'fail_mismatch'})
158*9c5db199SXin Li            self._shadow_store.commit(primary_info)
159*9c5db199SXin Li
160*9c5db199SXin Li
161*9c5db199SXin Lidef _log_info_mismatch(primary_info, shadow_info):
162*9c5db199SXin Li    """Log the two HostInfo instances.
163*9c5db199SXin Li
164*9c5db199SXin Li    Used as the default mismatch_callback.
165*9c5db199SXin Li    """
166*9c5db199SXin Li    logger.warning('primary / shadow disagree on refresh.')
167*9c5db199SXin Li    logger.warning('primary: %s', primary_info)
168*9c5db199SXin Li    logger.warning('shadow: %s', shadow_info)
169