Commit f8e9313d by Calen Pennington

Minimize the number of roundtrips to mongo during cms save by prefetching children

parent efc85b04
...@@ -54,7 +54,7 @@ def save_item(request): ...@@ -54,7 +54,7 @@ def save_item(request):
# This uses wildcarding to find the course, which requires handling # This uses wildcarding to find the course, which requires handling
# multiple courses returned, but there should only ever be one # multiple courses returned, but there should only ever be one
course_location = Location(item_id)._replace(category='course', name=None) course_location = Location(item_id)._replace(category='course', name=None)
courses = modulestore().get_items(course_location) courses = modulestore().get_items(course_location, depth=None)
for course in courses: for course in courses:
export_to_github(course, "CMS Edit") export_to_github(course, "CMS Edit")
......
...@@ -148,7 +148,7 @@ class ModuleStore(object): ...@@ -148,7 +148,7 @@ class ModuleStore(object):
""" """
An abstract interface for a database backend that stores XModuleDescriptor instances An abstract interface for a database backend that stores XModuleDescriptor instances
""" """
def get_item(self, location, default_class=None): def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the item with the most If location.revision is None, returns the item with the most
...@@ -159,20 +159,24 @@ class ModuleStore(object): ...@@ -159,20 +159,24 @@ class ModuleStore(object):
If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
location: Something that can be passed to Location location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found depth (int): An argument that some module stores may use to prefetch descendents of the queried modules
for more efficient results later in the request. The depth is counted in the number of
calls to get_children() to cache. None indicates to cache all descendents
""" """
raise NotImplementedError raise NotImplementedError
def get_items(self, location, default_class=None): def get_items(self, location, depth=0):
""" """
Returns a list of XModuleDescriptor instances for the items Returns a list of XModuleDescriptor instances for the items
that match location. Any element of location that is None is treated that match location. Any element of location that is None is treated
as a wildcard that matches any value as a wildcard that matches any value
location: Something that can be passed to Location location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found depth: An argument that some module stores may use to prefetch descendents of the queried modules
for more efficient results later in the request. The depth is counted in the number of calls
to get_children() to cache. None indicates to cache all descendents
""" """
raise NotImplementedError raise NotImplementedError
......
...@@ -15,7 +15,38 @@ from .exceptions import ItemNotFoundError, InsufficientSpecificationError ...@@ -15,7 +15,38 @@ from .exceptions import ItemNotFoundError, InsufficientSpecificationError
# that assumption will have to change # that assumption will have to change
class CachingDescriptorSystem(MakoDescriptorSystem):
"""
A system that has a cache of module json that it will use to load modules
from, with a backup of calling to the underlying modulestore for more data
"""
def __init__(self, modulestore, module_data, default_class, resources_fs, render_template):
"""
modulestore: the module store that can be used to retrieve additional modules
module_data: a mapping of Location -> json that was cached from the underlying modulestore
default_class: The default_class to use when loading an XModuleDescriptor from the module_data
resources_fs: a filesystem, as per MakoDescriptorSystem
render_template: a function for rendering templates, as per MakoDescriptorSystem
"""
super(CachingDescriptorSystem, self).__init__(render_template, self.load_item, resources_fs)
self.modulestore = modulestore
self.module_data = module_data
self.default_class = default_class
def load_item(self, location):
location = Location(location)
json_data = self.module_data.get(location)
if json_data is None:
return self.modulestore.get_item(location)
else:
return XModuleDescriptor.load_from_json(json_data, self, self.default_class)
def location_to_query(loc): def location_to_query(loc):
"""
Takes a Location and returns a SON object that will query for that location.
Fields in loc that are None are ignored in the query
"""
query = SON() query = SON()
# Location dict is ordered by specificity, and SON # Location dict is ordered by specificity, and SON
# will preserve that order for queries # will preserve that order for queries
...@@ -46,19 +77,49 @@ class MongoModuleStore(ModuleStore): ...@@ -46,19 +77,49 @@ class MongoModuleStore(ModuleStore):
class_ = getattr(import_module(module_path), class_name) class_ = getattr(import_module(module_path), class_name)
self.default_class = class_ self.default_class = class_
# TODO (cpennington): Pass a proper resources_fs to the system def _clean_item_data(self, item):
self.system = MakoDescriptorSystem( """
load_item=self.get_item, Renames the '_id' field in item to 'location'
resources_fs=None, """
render_template=render_to_string
)
def _load_item(self, item):
item['location'] = item['_id'] item['location'] = item['_id']
del item['_id'] del item['_id']
return XModuleDescriptor.load_from_json(item, self.system, self.default_class)
def get_item(self, location): def _cache_children(self, items, depth=0):
"""
Returns a dictionary mapping Location -> item data, populated with json data
for all descendents of items up to the specified depth.
This will make a number of queries that is linear in the depth
"""
data = {}
to_process = list(items)
while to_process and depth is None or depth >= 0:
children = []
for item in to_process:
self._clean_item_data(item)
children.extend(item.get('definition', {}).get('children', []))
data[Location(item['location'])] = item
# Load all children by id. See
# http://www.mongodb.org/display/DOCS/Advanced+Queries#AdvancedQueries-%24or
# for or-query syntax
if children:
to_process = list(self.collection.find({'_id': {'$in': [Location(child).dict() for child in children]}}))
else:
to_process = []
if depth is not None:
depth -= 1
return data
def _load_items(self, items, depth=0):
"""
Load a list of xmodules from the data in items, with children cached up to specified depth
"""
data_cache = self._cache_children(items, depth)
system = CachingDescriptorSystem(self, data_cache, self.default_class, None, render_to_string)
return [system.load_item(item['location']) for item in items]
def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the most item with the most If location.revision is None, returns the most item with the most
...@@ -68,7 +129,11 @@ class MongoModuleStore(ModuleStore): ...@@ -68,7 +129,11 @@ class MongoModuleStore(ModuleStore):
xmodule.modulestore.exceptions.InsufficientSpecificationError xmodule.modulestore.exceptions.InsufficientSpecificationError
If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
location: Something that can be passed to Location location: a Location object
depth (int): An argument that some module stores may use to prefetch descendents of the queried modules
for more efficient results later in the request. The depth is counted in the number of
calls to get_children() to cache. None indicates to cache all descendents
""" """
for key, val in Location(location).dict().iteritems(): for key, val in Location(location).dict().iteritems():
...@@ -81,15 +146,15 @@ class MongoModuleStore(ModuleStore): ...@@ -81,15 +146,15 @@ class MongoModuleStore(ModuleStore):
) )
if item is None: if item is None:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
return self._load_item(item) return self._load_items([item], depth)[0]
def get_items(self, location, default_class=None): def get_items(self, location, depth=0):
items = self.collection.find( items = self.collection.find(
location_to_query(location), location_to_query(location),
sort=[('revision', pymongo.ASCENDING)], sort=[('revision', pymongo.ASCENDING)],
) )
return [self._load_item(item) for item in items] return self._load_items(list(items), depth)
def create_item(self, location): def create_item(self, location):
""" """
......
...@@ -132,7 +132,7 @@ class XMLModuleStore(ModuleStore): ...@@ -132,7 +132,7 @@ class XMLModuleStore(ModuleStore):
log.debug('========> Done with course import') log.debug('========> Done with course import')
return course_descriptor return course_descriptor
def get_item(self, location): def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the most item with the most If location.revision is None, returns the most item with the most
...@@ -150,7 +150,7 @@ class XMLModuleStore(ModuleStore): ...@@ -150,7 +150,7 @@ class XMLModuleStore(ModuleStore):
except KeyError: except KeyError:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
def get_courses(self): def get_courses(self, depth=0):
""" """
Returns a list of course descriptors Returns a list of course descriptors
""" """
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment