thirdparty/google_appengine/google/appengine/tools/dev_appserver_index.py
changeset 109 620f9b141567
child 686 df109be0567c
equal deleted inserted replaced
108:261778de26ff 109:620f9b141567
       
     1 #!/usr/bin/env python
       
     2 #
       
     3 # Copyright 2007 Google Inc.
       
     4 #
       
     5 # Licensed under the Apache License, Version 2.0 (the "License");
       
     6 # you may not use this file except in compliance with the License.
       
     7 # You may obtain a copy of the License at
       
     8 #
       
     9 #     http://www.apache.org/licenses/LICENSE-2.0
       
    10 #
       
    11 # Unless required by applicable law or agreed to in writing, software
       
    12 # distributed under the License is distributed on an "AS IS" BASIS,
       
    13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    14 # See the License for the specific language governing permissions and
       
    15 # limitations under the License.
       
    16 #
       
    17 
       
    18 """Utilities for generating and updating index.yaml."""
       
    19 
       
    20 
       
    21 
       
    22 import os
       
    23 import logging
       
    24 
       
    25 from google.appengine.api import apiproxy_stub_map
       
    26 from google.appengine.api import datastore_admin
       
    27 from google.appengine.api import yaml_errors
       
    28 from google.appengine.datastore import datastore_index
       
    29 
       
    30 import yaml
       
    31 
       
    32 AUTO_MARKER = '\n# AUTOGENERATED\n'
       
    33 
       
    34 AUTO_COMMENT = '''
       
    35 # This index.yaml is automatically updated whenever the dev_appserver
       
    36 # detects that a new type of query is run.  If you want to manage the
       
    37 # index.yaml file manually, remove the above marker line (the line
       
    38 # saying "# AUTOGENERATED").  If you want to manage some indexes
       
    39 # manually, move them above the marker line.  The index.yaml file is
       
    40 # automatically uploaded to the admin console when you next deploy
       
    41 # your application using appcfg.py.
       
    42 '''
       
    43 
       
    44 
       
    45 def GenerateIndexFromHistory(query_history,
       
    46                              all_indexes=None, manual_indexes=None):
       
    47   """Generate most of the text for index.yaml from the query history.
       
    48 
       
    49   Args:
       
    50     query_history: Query history, a dict mapping query
       
    51     all_indexes: Optional datastore_index.IndexDefinitions instance
       
    52       representing all the indexes found in the input file.  May be None.
       
    53     manual_indexes: Optional datastore_index.IndexDefinitions instance
       
    54       containing indexes for which we should not generate output.  May be None.
       
    55 
       
    56   Returns:
       
    57     A string representation that can safely be appended to an
       
    58     existing index.yaml file.
       
    59   """
       
    60 
       
    61   all_keys = datastore_index.IndexDefinitionsToKeys(all_indexes)
       
    62   manual_keys = datastore_index.IndexDefinitionsToKeys(manual_indexes)
       
    63 
       
    64   indexes = dict((key, 0) for key in all_keys - manual_keys)
       
    65 
       
    66   for query, count in query_history.iteritems():
       
    67     key = datastore_index.CompositeIndexForQuery(query)
       
    68     if key is not None:
       
    69       key = key[:3]
       
    70       if key not in manual_keys:
       
    71         if key in indexes:
       
    72           indexes[key] += count
       
    73         else:
       
    74           indexes[key] = count
       
    75 
       
    76   res = []
       
    77   for (kind, ancestor, props), count in sorted(indexes.iteritems()):
       
    78     res.append('')
       
    79     if count == 0:
       
    80       message = '# Unused in query history -- copied from input.'
       
    81     elif count == 1:
       
    82       message = '# Used once in query history.'
       
    83     else:
       
    84       message = '# Used %d times in query history.' % count
       
    85     res.append(message)
       
    86     res.append(datastore_index.IndexYamlForQuery(kind, ancestor, props))
       
    87 
       
    88   res.append('')
       
    89   return '\n'.join(res)
       
    90 
       
    91 
       
    92 class IndexYamlUpdater(object):
       
    93   """Helper class for updating index.yaml.
       
    94 
       
    95   This class maintains some state about the query history and the
       
    96   index.yaml file in order to minimize the number of times index.yaml
       
    97   is actually overwritten.
       
    98   """
       
    99 
       
   100   index_yaml_is_manual = False
       
   101   index_yaml_mtime = 0
       
   102   last_history_size = 0
       
   103 
       
   104   def __init__(self, root_path):
       
   105     """Constructor.
       
   106 
       
   107     Args:
       
   108       root_path: Path to the app's root directory.
       
   109     """
       
   110     self.root_path = root_path
       
   111 
       
   112   def UpdateIndexYaml(self, openfile=open):
       
   113     """Update index.yaml.
       
   114 
       
   115     Args:
       
   116       openfile: Used for dependency injection.
       
   117 
       
   118     We only ever write to index.yaml if either:
       
   119     - it doesn't exist yet; or
       
   120     - it contains an 'AUTOGENERATED' comment.
       
   121 
       
   122     All indexes *before* the AUTOGENERATED comment will be written
       
   123     back unchanged.  All indexes *after* the AUTOGENERATED comment
       
   124     will be updated with the latest query counts (query counts are
       
   125     reset by --clear_datastore).  Indexes that aren't yet in the file
       
   126     will be appended to the AUTOGENERATED section.
       
   127 
       
   128     We keep track of some data in order to avoid doing repetitive work:
       
   129     - if index.yaml is fully manual, we keep track of its mtime to
       
   130       avoid parsing it over and over;
       
   131     - we keep track of the number of keys in the history dict since
       
   132       the last time we updated index.yaml (or decided there was
       
   133       nothing to update).
       
   134     """
       
   135     index_yaml_file = os.path.join(self.root_path, 'index.yaml')
       
   136 
       
   137     try:
       
   138       index_yaml_mtime = os.path.getmtime(index_yaml_file)
       
   139     except os.error:
       
   140       index_yaml_mtime = None
       
   141 
       
   142     index_yaml_changed = (index_yaml_mtime != self.index_yaml_mtime)
       
   143     self.index_yaml_mtime = index_yaml_mtime
       
   144 
       
   145     datastore_stub = apiproxy_stub_map.apiproxy.GetStub('datastore_v3')
       
   146     query_history = datastore_stub.QueryHistory()
       
   147     history_changed = (len(query_history) != self.last_history_size)
       
   148     self.last_history_size = len(query_history)
       
   149 
       
   150     if not (index_yaml_changed or history_changed):
       
   151       logging.debug('No need to update index.yaml')
       
   152       return
       
   153 
       
   154     if self.index_yaml_is_manual and not index_yaml_changed:
       
   155         logging.debug('Will not update manual index.yaml')
       
   156         return
       
   157 
       
   158     if index_yaml_mtime is None:
       
   159       index_yaml_data = None
       
   160     else:
       
   161       try:
       
   162         fh = open(index_yaml_file, 'r')
       
   163       except IOError:
       
   164         index_yaml_data = None
       
   165       else:
       
   166         try:
       
   167           index_yaml_data = fh.read()
       
   168         finally:
       
   169           fh.close()
       
   170 
       
   171     self.index_yaml_is_manual = (index_yaml_data is not None and
       
   172                                  AUTO_MARKER not in index_yaml_data)
       
   173     if self.index_yaml_is_manual:
       
   174       logging.info('Detected manual index.yaml, will not update')
       
   175       return
       
   176 
       
   177     if index_yaml_data is None:
       
   178       all_indexes = None
       
   179     else:
       
   180       try:
       
   181         all_indexes = datastore_index.ParseIndexDefinitions(index_yaml_data)
       
   182       except yaml_errors.EventListenerError, e:
       
   183         logging.error('Error parsing %s:\n%s', index_yaml_file, e)
       
   184         return
       
   185       except Exception, err:
       
   186         logging.error('Error parsing %s:\n%s.%s: %s', index_yaml_file,
       
   187                       err.__class__.__module__, err.__class__.__name__, err)
       
   188         return
       
   189 
       
   190     if index_yaml_data is None:
       
   191       manual_part, automatic_part = 'indexes:\n', ''
       
   192       manual_indexes = None
       
   193     else:
       
   194       manual_part, automatic_part = index_yaml_data.split(AUTO_MARKER, 1)
       
   195       try:
       
   196         manual_indexes = datastore_index.ParseIndexDefinitions(manual_part)
       
   197       except Exception, err:
       
   198         logging.error('Error parsing manual part of %s: %s',
       
   199                       index_yaml_file, err)
       
   200         return
       
   201 
       
   202     automatic_part = GenerateIndexFromHistory(query_history,
       
   203                                               all_indexes, manual_indexes)
       
   204 
       
   205     try:
       
   206       fh = openfile(index_yaml_file, 'w')
       
   207     except IOError, err:
       
   208       logging.error('Can\'t write index.yaml: %s', err)
       
   209       return
       
   210 
       
   211     try:
       
   212       logging.info('Updating %s', index_yaml_file)
       
   213       fh.write(manual_part)
       
   214       fh.write(AUTO_MARKER)
       
   215       fh.write(AUTO_COMMENT)
       
   216       fh.write(automatic_part)
       
   217     finally:
       
   218       fh.close()
       
   219 
       
   220     try:
       
   221       self.index_yaml_mtime = os.path.getmtime(index_yaml_file)
       
   222     except os.error, err:
       
   223       logging.error('Can\'t stat index.yaml we just wrote: %s', err)
       
   224       self.index_yaml_mtime = None
       
   225 
       
   226 
       
   227 def SetupIndexes(app_id, root_path):
       
   228   """Ensure that the set of existing composite indexes matches index.yaml.
       
   229 
       
   230   Note: this is similar to the algorithm used by the admin console for
       
   231   the same purpose.
       
   232 
       
   233   Args:
       
   234     app_id: Application ID being served.
       
   235     root_path: Path to the root of the application.
       
   236   """
       
   237   index_yaml_file = os.path.join(root_path, 'index.yaml')
       
   238   try:
       
   239     fh = open(index_yaml_file, 'r')
       
   240   except IOError:
       
   241     index_yaml_data = None
       
   242   else:
       
   243     try:
       
   244       index_yaml_data = fh.read()
       
   245     finally:
       
   246       fh.close()
       
   247 
       
   248   indexes = []
       
   249   if index_yaml_data is not None:
       
   250     index_defs = datastore_index.ParseIndexDefinitions(index_yaml_data)
       
   251     if index_defs is not None:
       
   252       indexes = index_defs.indexes
       
   253       if indexes is None:
       
   254         indexes = []
       
   255 
       
   256   requested_indexes = datastore_admin.IndexDefinitionsToProtos(app_id, indexes)
       
   257 
       
   258   existing_indexes = datastore_admin.GetIndices(app_id)
       
   259 
       
   260   requested = dict((x.definition().Encode(), x) for x in requested_indexes)
       
   261   existing = dict((x.definition().Encode(), x) for x in existing_indexes)
       
   262 
       
   263   created = 0
       
   264   for key, index in requested.iteritems():
       
   265     if key not in existing:
       
   266       datastore_admin.CreateIndex(index)
       
   267       created += 1
       
   268 
       
   269   deleted = 0
       
   270   for key, index in existing.iteritems():
       
   271     if key not in requested:
       
   272       datastore_admin.DeleteIndex(index)
       
   273       deleted += 1
       
   274 
       
   275   if created or deleted:
       
   276     logging.info("Created %d and deleted %d index(es); total %d",
       
   277                  created, deleted, len(requested))