Sync os-traits to Traits database table

When a new version of the os-traits library is released, the Traits
table in the api database needs to be updated to reflect those new
traits. This change does that, once, the first time either
Trait.get_by_name or TraitList.get_all is called in any process that is
using those objects.

This is an alternative to I729e84ea0ff8f333a156b24b15fe4d368209d015.
The major difference here is that there is no trait cache so the
surface area of the change is much smaller.

The list traits test in gabbit/traits.yaml has been changed to not
assert the length of traits returned: this can now change with each
release of the os-traits library.

Depends-on: I7a3f4bb8501fc3edad43e1aae5cb6b9ef1c0b00d
Co-Authored-By: Jay Pipes <jaypipes@gmail.com>
Change-Id: Ia92a4fd20b8991c6f4b34e3546cfb22aa5ed78aa
This commit is contained in:
Chris Dent 2017-05-31 16:38:00 +00:00
parent 3ce0a050e1
commit e013d1cabe
4 changed files with 112 additions and 7 deletions

View File

@ -15,6 +15,8 @@ import copy
# used over RPC. Remote manipulation is done with the placement HTTP
# API. The 'remotable' decorators should not be used.
import os_traits
from oslo_concurrency import lockutils
from oslo_db import exception as db_exc
from oslo_log import log as logging
from oslo_utils import versionutils
@ -43,6 +45,8 @@ _AGG_TBL = models.PlacementAggregate.__table__
_RP_AGG_TBL = models.ResourceProviderAggregate.__table__
_RP_TRAIT_TBL = models.ResourceProviderTrait.__table__
_RC_CACHE = None
_TRAIT_LOCK = 'trait_sync'
_TRAITS_SYNCED = False
LOG = logging.getLogger(__name__)
@ -61,6 +65,70 @@ def _ensure_rc_cache(ctx):
_RC_CACHE = rc_cache.ResourceClassCache(ctx)
@db_api.api_context_manager.writer
def _trait_sync(ctx):
"""Sync the os_traits symbols to the database.
Reads all symbols from the os_traits library, checks if any of them do
not exist in the database and bulk-inserts those that are not. This is
done once per process using this code if either Trait.get_by_name or
TraitList.get_all is called.
:param ctx: `nova.context.RequestContext` that may be used to grab a DB
connection.
"""
# Create a set of all traits in the os_traits library.
std_traits = set(os_traits.get_traits())
conn = ctx.session.connection()
sel = sa.select([_TRAIT_TBL.c.name])
res = conn.execute(sel).fetchall()
# Create a set of all traits in the db that are not custom
# traits.
db_traits = set(
r[0] for r in res
if not os_traits.is_custom(r[0])
)
# Determine those traits which are in os_traits but not
# currently in the database, and insert them.
need_sync = std_traits - db_traits
ins = _TRAIT_TBL.insert()
batch_args = [
{'name': six.text_type(trait)}
for trait in need_sync
]
if batch_args:
try:
conn.execute(ins, batch_args)
LOG.info("Synced traits from os_traits into API DB: %s",
need_sync)
except db_exc.DBDuplicateEntry:
pass # some other process sync'd, just ignore
def _ensure_trait_sync(ctx):
"""Ensures that the os_traits library is synchronized to the traits db.
If _TRAITS_SYNCED is False then this process has not tried to update the
traits db. Do so by calling _trait_sync. Since the placement API server
could be multi-threaded, lock around testing _TRAITS_SYNCED to avoid
duplicating work.
Different placement API server processes that talk to the same database
will avoid issues through the power of transactions.
:param ctx: `nova.context.RequestContext` that may be used to grab a DB
connection.
"""
global _TRAITS_SYNCED
# If another thread is doing this work, wait for it to complete.
# When that thread is done _TRAITS_SYNCED will be true in this
# thread and we'll simply return.
with lockutils.lock(_TRAIT_LOCK):
if not _TRAITS_SYNCED:
_trait_sync(ctx)
_TRAITS_SYNCED = True
def _get_current_inventory_resources(conn, rp):
"""Returns a set() containing the resource class IDs for all resources
currently having an inventory record for the supplied resource provider.
@ -2020,8 +2088,9 @@ class Trait(base.NovaObject):
self._from_db_object(self._context, self, db_trait)
@staticmethod
@db_api.api_context_manager.reader
@db_api.api_context_manager.writer # trait sync can cause a write
def _get_by_name_from_db(context, name):
_ensure_trait_sync(context)
result = context.session.query(models.Trait).filter_by(
name=name).first()
if not result:
@ -2071,8 +2140,9 @@ class TraitList(base.ObjectListBase, base.NovaObject):
}
@staticmethod
@db_api.api_context_manager.reader
@db_api.api_context_manager.writer # trait sync can cause a write
def _get_all_from_db(context, filters):
_ensure_trait_sync(context)
if not filters:
filters = {}

View File

@ -71,14 +71,17 @@ tests:
response_forbidden_headers:
- content-type
# NOTE(cdent): This simply tests that traits we know should be
# present are in the results. We can't check length here because
# the standard traits, which will grow over time, are present.
- name: list traits
GET: /traits
status: 200
response_json_paths:
$.traits.`len`: 2
response_strings:
- CUSTOM_TRAIT_1
- CUSTOM_TRAIT_2
- MISC_SHARES_VIA_AGGREGATE
- HW_CPU_X86_SHA
- name: list traits with invalid format of name parameter
GET: /traits?name=in_abc

View File

@ -12,7 +12,9 @@
import mock
import os_traits
from oslo_db import exception as db_exc
import sqlalchemy as sa
import nova
from nova import context
@ -1534,6 +1536,11 @@ class ResourceClassTestCase(ResourceProviderBaseCase):
class ResourceProviderTraitTestCase(ResourceProviderBaseCase):
def tearDown(self):
"""Reset the _TRAITS_SYNCED boolean so it doesn't interfere."""
super(ResourceProviderTraitTestCase, self).tearDown()
rp_obj._TRAITS_SYNCED = False
def _assert_traits(self, expected_traits, traits_objs):
expected_traits.sort()
traits = []
@ -1542,6 +1549,11 @@ class ResourceProviderTraitTestCase(ResourceProviderBaseCase):
traits.sort()
self.assertEqual(expected_traits, traits)
def _assert_traits_in(self, expected_traits, traits_objs):
traits = [trait.name for trait in traits_objs]
for expected in expected_traits:
self.assertIn(expected, traits)
def test_trait_create(self):
t = objects.Trait(self.context)
t.name = 'CUSTOM_TRAIT_A'
@ -1603,8 +1615,8 @@ class ResourceProviderTraitTestCase(ResourceProviderBaseCase):
t.name = name
t.create()
self._assert_traits(trait_names,
objects.TraitList.get_all(self.context))
self._assert_traits_in(trait_names,
objects.TraitList.get_all(self.context))
def test_traits_get_all_with_name_in_filter(self):
trait_names = ['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B', 'CUSTOM_TRAIT_C']
@ -1734,10 +1746,29 @@ class ResourceProviderTraitTestCase(ResourceProviderBaseCase):
filters={'name_in': ['CUSTOM_TRAIT_A', 'CUSTOM_TRAIT_B']})
rp1.set_traits(associated_traits)
rp2.set_traits(associated_traits)
self._assert_traits(['CUSTOM_TRAIT_C'],
self._assert_traits_in(['CUSTOM_TRAIT_C'],
objects.TraitList.get_all(self.context,
filters={'associated': False}))
def test_sync_standard_traits(self):
"""Tests that on a clean DB, we have zero traits in the DB but after
list all traits, os_traits have been synchronized.
"""
std_traits = os_traits.get_traits()
conn = self.api_db.get_engine().connect()
def _db_traits(conn):
sel = sa.select([rp_obj._TRAIT_TBL.c.name])
return [r[0] for r in conn.execute(sel).fetchall()]
self.assertEqual([], _db_traits(conn))
all_traits = [trait.name for trait in
objects.TraitList.get_all(self.context)]
self.assertEqual(set(std_traits), set(all_traits))
# confirm with a raw request
self.assertEqual(set(std_traits), set(_db_traits(conn)))
class SharedProviderTestCase(ResourceProviderBaseCase):
"""Tests that the queries used to determine placement in deployments with

View File

@ -53,6 +53,7 @@ oslo.middleware>=3.27.0 # Apache-2.0
psutil>=3.2.2 # BSD
oslo.versionedobjects>=1.17.0 # Apache-2.0
os-brick>=1.13.1 # Apache-2.0
os-traits>=0.3.1 # Apache-2.0
os-vif>=1.4.0 # Apache-2.0
os-win>=2.0.0 # Apache-2.0
castellan>=0.7.0 # Apache-2.0