You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
169 lines
6.7 KiB
169 lines
6.7 KiB
# 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)
|