a8a349c10f
In order to avoid ballooning memory usage we should limit the number of instances pulled at any time while syncing them. This adds pagination to the generator used in order to limit it, and an optional parameter to optimize for when small numbers of instances are needed. Change-Id: Ia82185af27ed11d68a8498ec4bde3da529134893 Closes-Bug: 1447732
219 lines
7.1 KiB
Python
219 lines
7.1 KiB
Python
# Copyright (c) 2012 Rackspace Hosting
|
|
# All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
"""
|
|
Cells Utility Methods
|
|
"""
|
|
import random
|
|
import sys
|
|
|
|
from oslo_config import cfg
|
|
import six
|
|
|
|
from nova import objects
|
|
from nova.objects import base as obj_base
|
|
|
|
|
|
# Separator used between cell names for the 'full cell name' and routing
|
|
# path
|
|
PATH_CELL_SEP = '!'
|
|
# Flag prepended to a cell name to indicate data shouldn't be synced during
|
|
# an instance save. There are no illegal chars in a cell name so using the
|
|
# meaningful PATH_CELL_SEP in an invalid way will need to suffice.
|
|
BLOCK_SYNC_FLAG = '!!'
|
|
# Separator used between cell name and item
|
|
_CELL_ITEM_SEP = '@'
|
|
|
|
CONF = cfg.CONF
|
|
CONF.import_opt('instance_update_sync_database_limit', 'nova.cells.opts',
|
|
group='cells')
|
|
|
|
|
|
class ProxyObjectSerializer(obj_base.NovaObjectSerializer):
|
|
def __init__(self):
|
|
super(ProxyObjectSerializer, self).__init__()
|
|
self.serializer = super(ProxyObjectSerializer, self)
|
|
|
|
def _process_object(self, context, objprim):
|
|
return _CellProxy.obj_from_primitive(self.serializer, objprim, context)
|
|
|
|
|
|
class _CellProxy(object):
|
|
def __init__(self, obj, cell_path):
|
|
self._obj = obj
|
|
self._cell_path = cell_path
|
|
|
|
@property
|
|
def id(self):
|
|
return cell_with_item(self._cell_path, self._obj.id)
|
|
|
|
@property
|
|
def host(self):
|
|
return cell_with_item(self._cell_path, self._obj.host)
|
|
|
|
def __getitem__(self, key):
|
|
if key == 'id':
|
|
return self.id
|
|
if key == 'host':
|
|
return self.host
|
|
|
|
return getattr(self._obj, key)
|
|
|
|
def obj_to_primitive(self):
|
|
obj_p = self._obj.obj_to_primitive()
|
|
obj_p['cell_proxy.class_name'] = self.__class__.__name__
|
|
obj_p['cell_proxy.cell_path'] = self._cell_path
|
|
return obj_p
|
|
|
|
@classmethod
|
|
def obj_from_primitive(cls, serializer, primitive, context=None):
|
|
obj_primitive = primitive.copy()
|
|
cell_path = obj_primitive.pop('cell_proxy.cell_path', None)
|
|
klass_name = obj_primitive.pop('cell_proxy.class_name', None)
|
|
obj = serializer._process_object(context, obj_primitive)
|
|
if klass_name is not None and cell_path is not None:
|
|
klass = getattr(sys.modules[__name__], klass_name)
|
|
return klass(obj, cell_path)
|
|
else:
|
|
return obj
|
|
|
|
# dict-ish syntax sugar
|
|
def _iteritems(self):
|
|
"""For backwards-compatibility with dict-based objects.
|
|
|
|
NOTE(sbauza): May be removed in the future.
|
|
"""
|
|
for name in self._obj.obj_fields:
|
|
if (self._obj.obj_attr_is_set(name) or
|
|
name in self._obj.obj_extra_fields):
|
|
if name == 'id':
|
|
yield name, self.id
|
|
elif name == 'host':
|
|
yield name, self.host
|
|
else:
|
|
yield name, getattr(self._obj, name)
|
|
|
|
if six.PY3:
|
|
items = _iteritems
|
|
else:
|
|
iteritems = _iteritems
|
|
|
|
def __getattr__(self, key):
|
|
return getattr(self._obj, key)
|
|
|
|
|
|
class ComputeNodeProxy(_CellProxy):
|
|
pass
|
|
|
|
|
|
class ServiceProxy(_CellProxy):
|
|
def __getattr__(self, key):
|
|
if key == 'compute_node':
|
|
# NOTE(sbauza): As the Service object is still having a nested
|
|
# ComputeNode object that consumers of this Proxy don't use, we can
|
|
# safely remove it from what's returned
|
|
raise AttributeError
|
|
return getattr(self._obj, key)
|
|
|
|
|
|
def get_instances_to_sync(context, updated_since=None, project_id=None,
|
|
deleted=True, shuffle=False, uuids_only=False):
|
|
"""Return a generator that will return a list of active and
|
|
deleted instances to sync with parent cells. The list may
|
|
optionally be shuffled for periodic updates so that multiple
|
|
cells services aren't self-healing the same instances in nearly
|
|
lockstep.
|
|
"""
|
|
def _get_paginated_instances(context, filters, shuffle, limit, marker):
|
|
instances = objects.InstanceList.get_by_filters(
|
|
context, filters, sort_key='deleted', sort_dir='asc',
|
|
limit=limit, marker=marker)
|
|
if len(instances) > 0:
|
|
marker = instances[-1]['uuid']
|
|
# NOTE(melwitt/alaski): Need a list that supports assignment for
|
|
# shuffle. And pop() on the returned result.
|
|
instances = list(instances)
|
|
if shuffle:
|
|
random.shuffle(instances)
|
|
return instances, marker
|
|
|
|
filters = {}
|
|
if updated_since is not None:
|
|
filters['changes-since'] = updated_since
|
|
if project_id is not None:
|
|
filters['project_id'] = project_id
|
|
if not deleted:
|
|
filters['deleted'] = False
|
|
# Active instances first.
|
|
limit = CONF.cells.instance_update_sync_database_limit
|
|
marker = None
|
|
|
|
instances = []
|
|
while True:
|
|
if not instances:
|
|
instances, marker = _get_paginated_instances(context, filters,
|
|
shuffle, limit, marker)
|
|
if not instances:
|
|
break
|
|
instance = instances.pop(0)
|
|
if uuids_only:
|
|
yield instance.uuid
|
|
else:
|
|
yield instance
|
|
|
|
|
|
def cell_with_item(cell_name, item):
|
|
"""Turn cell_name and item into <cell_name>@<item>."""
|
|
if cell_name is None:
|
|
return item
|
|
return cell_name + _CELL_ITEM_SEP + str(item)
|
|
|
|
|
|
def split_cell_and_item(cell_and_item):
|
|
"""Split a combined cell@item and return them."""
|
|
result = cell_and_item.rsplit(_CELL_ITEM_SEP, 1)
|
|
if len(result) == 1:
|
|
return (None, cell_and_item)
|
|
else:
|
|
return result
|
|
|
|
|
|
def add_cell_to_compute_node(compute_node, cell_name):
|
|
"""Fix compute_node attributes that should be unique. Allows
|
|
API cell to query the 'id' by cell@id.
|
|
"""
|
|
# NOTE(sbauza): As compute_node is a ComputeNode object, we need to wrap it
|
|
# for adding the cell_path information
|
|
compute_proxy = ComputeNodeProxy(compute_node, cell_name)
|
|
return compute_proxy
|
|
|
|
|
|
def add_cell_to_service(service, cell_name):
|
|
"""Fix service attributes that should be unique. Allows
|
|
API cell to query the 'id' or 'host' by cell@id/host.
|
|
"""
|
|
# NOTE(sbauza): As service is a Service object, we need to wrap it
|
|
# for adding the cell_path information
|
|
service_proxy = ServiceProxy(service, cell_name)
|
|
return service_proxy
|
|
|
|
|
|
def add_cell_to_task_log(task_log, cell_name):
|
|
"""Fix task_log attributes that should be unique. In particular,
|
|
the 'id' and 'host' fields should be prepended with cell name.
|
|
"""
|
|
task_log['id'] = cell_with_item(cell_name, task_log['id'])
|
|
task_log['host'] = cell_with_item(cell_name, task_log['host'])
|