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