Fix instance.hidden migration and querying
It was discovered that default= on a Column definition in a schema migration will attempt to update the table with the provided value, instead of just translating on read, which is often the assumption. The Instance.hidden=False change introduced in Train[1] used such a default on the new column, which caused at least one real-world deployment to time out rewriting the instances table due to size. Apparently SQLAlchemy-migrate also does not consider such a timeout to be a failure and proceeds on. The end result is that some existing instances in the database have hidden=NULL values, and the DB model layer does not convert those to hidden=False when we read/query them, causing those instances to be excluded from the API list view. This change alters the 399 schema migration to remove the default=False specification. This does not actually change the schema, but /will/ prevent users who have not yet upgraded to Train from rewriting the table. This change also makes the instance_get_all_by_filters() code handle hidden specially, including false and NULL in a query for non-hidden instances. A future change should add a developer trap test to ensure that future migrations do not add default= values to new columns to avoid this situation in the future. [1] Iaffb27bd8c562ba120047c04bb62619c0864f594 Change-Id: Iace3f653b42c20887b40ee0105c8e9a4edeff1f7 Closes-Bug: #1862205
This commit is contained in:
parent
014c1ab864
commit
001f3a7bfe
@ -2205,9 +2205,15 @@ def instance_get_all_by_filters_sort(context, filters, limit=None, marker=None,
|
|||||||
else:
|
else:
|
||||||
filters['user_id'] = context.user_id
|
filters['user_id'] = context.user_id
|
||||||
|
|
||||||
if 'hidden' not in filters:
|
if filters.pop('hidden', False):
|
||||||
# Filter out hidden instances by default.
|
query_prefix = query_prefix.filter(models.Instance.hidden == true())
|
||||||
filters['hidden'] = False
|
else:
|
||||||
|
# If the query should not include hidden instances, then
|
||||||
|
# filter instances with hidden=False or hidden=NULL because
|
||||||
|
# older records may have no value set.
|
||||||
|
query_prefix = query_prefix.filter(or_(
|
||||||
|
models.Instance.hidden == false(),
|
||||||
|
models.Instance.hidden == null()))
|
||||||
|
|
||||||
# Filters for exact matches that we can do along with the SQL query...
|
# Filters for exact matches that we can do along with the SQL query...
|
||||||
# For other filters that don't match this, we will do regexp matching
|
# For other filters that don't match this, we will do regexp matching
|
||||||
|
@ -20,5 +20,9 @@ def upgrade(migrate_engine):
|
|||||||
for prefix in ('', 'shadow_'):
|
for prefix in ('', 'shadow_'):
|
||||||
instances = Table('%sinstances' % prefix, meta, autoload=True)
|
instances = Table('%sinstances' % prefix, meta, autoload=True)
|
||||||
if not hasattr(instances.c, 'hidden'):
|
if not hasattr(instances.c, 'hidden'):
|
||||||
hidden = Column('hidden', Boolean, default=False)
|
# NOTE(danms): This column originally included default=False. We
|
||||||
|
# discovered in bug #1862205 that this will attempt to rewrite
|
||||||
|
# the entire instances table with that value, which can time out
|
||||||
|
# for large data sets (and does not even abort).
|
||||||
|
hidden = Column('hidden', Boolean)
|
||||||
instances.create_column(hidden)
|
instances.create_column(hidden)
|
||||||
|
@ -1517,7 +1517,9 @@ class InstanceList(base.ObjectListBase, base.NovaObject):
|
|||||||
# NOTE(mriedem): Filter out hidden instances since there should be a
|
# NOTE(mriedem): Filter out hidden instances since there should be a
|
||||||
# non-hidden version of the instance in another cell database and the
|
# non-hidden version of the instance in another cell database and the
|
||||||
# API will only show one of them, so we don't count the hidden copy.
|
# API will only show one of them, so we don't count the hidden copy.
|
||||||
project_query = project_query.filter_by(hidden=false())
|
project_query = project_query.filter(
|
||||||
|
or_(models.Instance.hidden == false(),
|
||||||
|
models.Instance.hidden == null()))
|
||||||
|
|
||||||
project_result = project_query.first()
|
project_result = project_query.first()
|
||||||
fields = ('instances', 'cores', 'ram')
|
fields = ('instances', 'cores', 'ram')
|
||||||
|
@ -27,6 +27,8 @@ from oslo_versionedobjects import base as ovo_base
|
|||||||
from nova.compute import task_states
|
from nova.compute import task_states
|
||||||
from nova.compute import vm_states
|
from nova.compute import vm_states
|
||||||
from nova.db import api as db
|
from nova.db import api as db
|
||||||
|
from nova.db.sqlalchemy import api as sql_api
|
||||||
|
from nova.db.sqlalchemy import models as sql_models
|
||||||
from nova import exception
|
from nova import exception
|
||||||
from nova.network import model as network_model
|
from nova.network import model as network_model
|
||||||
from nova import notifications
|
from nova import notifications
|
||||||
@ -1964,7 +1966,53 @@ class _TestInstanceListObject(object):
|
|||||||
|
|
||||||
class TestInstanceListObject(test_objects._LocalTest,
|
class TestInstanceListObject(test_objects._LocalTest,
|
||||||
_TestInstanceListObject):
|
_TestInstanceListObject):
|
||||||
pass
|
# No point in doing this db-specific test twice for remote
|
||||||
|
def test_hidden_filter_query(self):
|
||||||
|
"""Check that our instance_get_by_filters() honors hidden properly
|
||||||
|
|
||||||
|
As reported in bug #1862205, we need to properly handle instances
|
||||||
|
with the hidden field set to NULL and not expect SQLAlchemy to
|
||||||
|
translate those values on SELECT.
|
||||||
|
"""
|
||||||
|
values = {'user_id': self.context.user_id,
|
||||||
|
'project_id': self.context.project_id,
|
||||||
|
'host': 'foo'}
|
||||||
|
|
||||||
|
for hidden_value in (True, False):
|
||||||
|
db.instance_create(self.context,
|
||||||
|
dict(values, hidden=hidden_value))
|
||||||
|
|
||||||
|
# NOTE(danms): Because the model has default=False, we can not use
|
||||||
|
# it to create an instance with a hidden value of NULL. So, do it
|
||||||
|
# manually here.
|
||||||
|
engine = sql_api.get_engine()
|
||||||
|
table = sql_models.Instance.__table__
|
||||||
|
with engine.connect() as conn:
|
||||||
|
update = table.insert().values(user_id=self.context.user_id,
|
||||||
|
project_id=self.context.project_id,
|
||||||
|
uuid=uuids.nullinst,
|
||||||
|
host='foo',
|
||||||
|
hidden=None)
|
||||||
|
conn.execute(update)
|
||||||
|
|
||||||
|
insts = objects.InstanceList.get_by_filters(self.context,
|
||||||
|
{'hidden': True})
|
||||||
|
# We created one hidden instance above, so expect only that one
|
||||||
|
# to come out of this query.
|
||||||
|
self.assertEqual(1, len(insts))
|
||||||
|
|
||||||
|
# We created one unhidden instance above, and one specifically
|
||||||
|
# with a NULL value to represent an unmigrated instance, which
|
||||||
|
# defaults to hidden=False, so expect both of those here.
|
||||||
|
insts = objects.InstanceList.get_by_filters(self.context,
|
||||||
|
{'hidden': False})
|
||||||
|
self.assertEqual(2, len(insts))
|
||||||
|
|
||||||
|
# Do the same check as above, but make sure hidden=False is the
|
||||||
|
# default behavior.
|
||||||
|
insts = objects.InstanceList.get_by_filters(self.context,
|
||||||
|
{})
|
||||||
|
self.assertEqual(2, len(insts))
|
||||||
|
|
||||||
|
|
||||||
class TestRemoteInstanceListObject(test_objects._RemoteTest,
|
class TestRemoteInstanceListObject(test_objects._RemoteTest,
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
---
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
Upgrading to Train on a deployment with a large database may hit
|
||||||
|
`bug 1862205`_, which results in instance records left in a bad
|
||||||
|
state, and manifests as instances not being shown in list
|
||||||
|
operations. Users upgrading to Train for the first time will
|
||||||
|
definitely want to apply a version which includes this fix. Users
|
||||||
|
already on Train should upgrade to a version including this fix to
|
||||||
|
ensure the problem is addressed.
|
||||||
|
|
||||||
|
.. _bug 1862205: https://launchpad.net/bugs/1862205
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
A fix for serious `bug 1862205`_ is provided which addresses both
|
||||||
|
the performance aspect of schema migration 399, as well as the
|
||||||
|
potential fallout for cases where this migration silently fails
|
||||||
|
and leaves large numbers of instances hidden from view from the
|
||||||
|
API.
|
Loading…
Reference in New Issue
Block a user