# Copyright 2017 The Chromium OS Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. from __future__ import absolute_import from __future__ import print_function import logging import common from autotest_lib.server.hosts import host_info from chromite.lib import metrics _METRICS_PREFIX = 'chromeos/autotest/autoserv/host_info/shadowing_store/' _REFRESH_METRIC_NAME = _METRICS_PREFIX + 'refresh_count' _COMMIT_METRIC_NAME = _METRICS_PREFIX + 'commit_count' logger = logging.getLogger(__file__) class ShadowingStore(host_info.CachingHostInfoStore): """A composite CachingHostInfoStore that maintains a main and shadow store. ShadowingStore accepts two CachingHostInfoStore objects - primary_store and shadow_store. All refresh/commit operations are serviced through primary_store. In addition, shadow_store is updated and compared with this information, leaving breadcrumbs when the two differ. Any errors in shadow_store operations are logged and ignored so as to not affect the user. This is a transitional CachingHostInfoStore that allows us to continue to use an AfeStore in practice, but also create a backing FileStore so that we can validate the use of FileStore in prod. """ def __init__(self, primary_store, shadow_store, mismatch_callback=None): """ @param primary_store: A CachingHostInfoStore to be used as the primary store. @param shadow_store: A CachingHostInfoStore to be used to shadow the primary store. @param mismatch_callback: A callback used to notify whenever we notice a mismatch between primary_store and shadow_store. The signature of the callback must match: callback(primary_info, shadow_info) where primary_info and shadow_info are HostInfo objects obtained from the two stores respectively. Mostly used by unittests. Actual users don't know / nor care that they're using a ShadowingStore. """ super(ShadowingStore, self).__init__() self._primary_store = primary_store self._shadow_store = shadow_store self._mismatch_callback = ( mismatch_callback if mismatch_callback is not None else _log_info_mismatch) try: self._shadow_store.commit(self._primary_store.get()) except host_info.StoreError as e: metrics.Counter( _METRICS_PREFIX + 'initialization_fail_count').increment() logger.exception( 'Failed to initialize shadow store. ' 'Expect primary / shadow desync in the future.') def commit_with_substitute(self, info, primary_store=None, shadow_store=None): """Commit host information using alternative stores. This is used to commit using an alternative store implementation to work around some issues (crbug.com/903589). Don't set cached_info in this function. @param info: A HostInfo object to set. @param primary_store: A CachingHostInfoStore object to commit instead of the original primary_store. @param shadow_store: A CachingHostInfoStore object to commit instead of the original shadow store. """ if primary_store is not None: primary_store.commit(info) else: self._commit_to_primary_store(info) if shadow_store is not None: shadow_store.commit(info) else: self._commit_to_shadow_store(info) def __str__(self): return '%s[%s, %s]' % (type(self).__name__, self._primary_store, self._shadow_store) def _refresh_impl(self): """Obtains HostInfo from the primary and compares against shadow""" primary_info = self._refresh_from_primary_store() try: shadow_info = self._refresh_from_shadow_store() except host_info.StoreError: logger.exception('Shadow refresh failed. ' 'Skipping comparison with primary.') return primary_info self._verify_store_infos(primary_info, shadow_info) return primary_info def _commit_impl(self, info): """Commits HostInfo to both the primary and shadow store""" self._commit_to_primary_store(info) self._commit_to_shadow_store(info) def _commit_to_primary_store(self, info): try: self._primary_store.commit(info) except host_info.StoreError: metrics.Counter(_COMMIT_METRIC_NAME).increment( fields={'file_commit_result': 'skipped'}) raise def _commit_to_shadow_store(self, info): try: self._shadow_store.commit(info) except host_info.StoreError: logger.exception( 'shadow commit failed. ' 'Expect primary / shadow desync in the future.') metrics.Counter(_COMMIT_METRIC_NAME).increment( fields={'file_commit_result': 'fail'}) else: metrics.Counter(_COMMIT_METRIC_NAME).increment( fields={'file_commit_result': 'success'}) def _refresh_from_primary_store(self): try: return self._primary_store.get(force_refresh=True) except host_info.StoreError: metrics.Counter(_REFRESH_METRIC_NAME).increment( fields={'validation_result': 'skipped'}) raise def _refresh_from_shadow_store(self): try: return self._shadow_store.get(force_refresh=True) except host_info.StoreError: metrics.Counter(_REFRESH_METRIC_NAME).increment(fields={ 'validation_result': 'fail_shadow_store_refresh'}) raise def _verify_store_infos(self, primary_info, shadow_info): if primary_info == shadow_info: metrics.Counter(_REFRESH_METRIC_NAME).increment( fields={'validation_result': 'success'}) else: self._mismatch_callback(primary_info, shadow_info) metrics.Counter(_REFRESH_METRIC_NAME).increment( fields={'validation_result': 'fail_mismatch'}) self._shadow_store.commit(primary_info) def _log_info_mismatch(primary_info, shadow_info): """Log the two HostInfo instances. Used as the default mismatch_callback. """ logger.warning('primary / shadow disagree on refresh.') logger.warning('primary: %s', primary_info) logger.warning('shadow: %s', shadow_info)