thirdparty/google_appengine/google/appengine/tools/appcfg.py
changeset 1278 a7766286a7be
parent 828 f5fd65cc3bf3
child 2172 ac7bd3b467ff
--- a/thirdparty/google_appengine/google/appengine/tools/appcfg.py	Thu Feb 12 10:24:37 2009 +0000
+++ b/thirdparty/google_appengine/google/appengine/tools/appcfg.py	Thu Feb 12 12:30:36 2009 +0000
@@ -71,6 +71,10 @@
 
 
 appinfo.AppInfoExternal.ATTRIBUTES[appinfo.RUNTIME] = "python"
+_api_versions = os.environ.get('GOOGLE_TEST_API_VERSIONS', '1')
+_options = validation.Options(*_api_versions.split(','))
+appinfo.AppInfoExternal.ATTRIBUTES[appinfo.API_VERSION] = _options
+del _api_versions, _options
 
 
 def StatusUpdate(msg):
@@ -189,6 +193,29 @@
 
   return version
 
+def RetryWithBackoff(initial_delay, backoff_factor, max_tries, callable):
+    """Calls a function multiple times, backing off more and more each time.
+
+    Args:
+      initial_delay: Initial delay after first try, in seconds.
+      backoff_factor: Delay will be multiplied by this factor after each try.
+      max_tries: Maximum number of tries.
+      callable: The method to call, will pass no arguments.
+
+    Returns:
+      True if the function succeded in one of its tries.
+
+    Raises:
+      Whatever the function raises--an exception will immediately stop retries.
+    """
+    delay = initial_delay
+    while not callable() and max_tries > 0:
+      StatusUpdate("Will check again in %s seconds." % delay)
+      time.sleep(delay)
+      delay *= backoff_factor
+      max_tries -= 1
+    return max_tries > 0
+
 
 class UpdateCheck(object):
   """Determines if the local SDK is the latest version.
@@ -789,7 +816,7 @@
   header uses UTC), and the client's local time is irrelevant.
 
   Args:
-    A posix timestamp giving current UTC time.
+    now: A posix timestamp giving current UTC time.
 
   Returns:
     A pseudo-posix timestamp giving current Pacific time.  Passing
@@ -913,6 +940,7 @@
       hash of the file contents.
     in_transaction: True iff a transaction with the server has started.
       An AppVersionUpload can do only one transaction at a time.
+    deployed: True iff the Deploy method has been called.
   """
 
   def __init__(self, server, config):
@@ -930,6 +958,7 @@
     self.version = self.config.version
     self.files = {}
     self.in_transaction = False
+    self.deployed = False
 
   def _Hash(self, content):
     """Compute the hash of the content.
@@ -1059,6 +1088,8 @@
     All the files returned by Begin() must have been uploaded with UploadFile()
     before Commit() can be called.
 
+    This tries the new 'deploy' method; if that fails it uses the old 'commit'.
+
     Raises:
       Exception: Some required files were not uploaded.
     """
@@ -1066,10 +1097,65 @@
     if self.files:
       raise Exception("Not all required files have been uploaded.")
 
-    StatusUpdate("Closing update.")
-    self.server.Send("/api/appversion/commit", app_id=self.app_id,
+    try:
+      self.Deploy()
+      if not RetryWithBackoff(1, 2, 8, self.IsReady):
+        logging.warning("Version still not ready to serve, aborting.")
+        raise Exception("Version not ready.")
+      self.StartServing()
+    except urllib2.HTTPError, e:
+      if e.code != 404:
+        raise
+      StatusUpdate("Closing update.")
+      self.server.Send("/api/appversion/commit", app_id=self.app_id,
+                       version=self.version)
+      self.in_transaction = False
+
+  def Deploy(self):
+    """Deploys the new app version but does not make it default.
+
+    All the files returned by Begin() must have been uploaded with UploadFile()
+    before Deploy() can be called.
+
+    Raises:
+      Exception: Some required files were not uploaded.
+    """
+    assert self.in_transaction, "Begin() must be called before Deploy()."
+    if self.files:
+      raise Exception("Not all required files have been uploaded.")
+
+    StatusUpdate("Deploying new version.")
+    self.server.Send("/api/appversion/deploy", app_id=self.app_id,
                      version=self.version)
-    self.in_transaction = False
+    self.deployed = True
+
+  def IsReady(self):
+    """Check if the new app version is ready to serve traffic.
+
+    Raises:
+      Exception: Deploy has not yet been called.
+
+    Returns:
+      True if the server returned the app is ready to serve.
+    """
+    assert self.deployed, "Deploy() must be called before IsReady()."
+
+    StatusUpdate("Checking if new version is ready to serve.")
+    result = self.server.Send("/api/appversion/isready", app_id=self.app_id,
+                              version=self.version)
+    return result == "1"
+
+  def StartServing(self):
+    """Start serving with the newly created version.
+
+    Raises:
+      Exception: Deploy has not yet been called.
+    """
+    assert self.deployed, "Deploy() must be called before IsReady()."
+
+    StatusUpdate("Closing update: new version is ready to start serving.")
+    self.server.Send("/api/appversion/startserving",
+                     app_id=self.app_id, version=self.version)
 
   def Rollback(self):
     """Rolls back the transaction if one is in progress."""
@@ -1140,12 +1226,13 @@
             StatusUpdate("Uploaded %d files." % num_files)
 
       self.Commit()
+
     except KeyboardInterrupt:
       logging.info("User interrupted. Aborting.")
       self.Rollback()
       raise
     except:
-      logging.error("An unexpected error occurred. Aborting.")
+      logging.exception("An unexpected error occurred. Aborting.")
       self.Rollback()
       raise
 
@@ -1271,7 +1358,8 @@
                rpc_server_class=appengine_rpc.HttpRpcServer,
                raw_input_fn=raw_input,
                password_input_fn=getpass.getpass,
-               error_fh=sys.stderr):
+               error_fh=sys.stderr,
+               update_check_class=UpdateCheck):
     """Initializer.  Parses the cmdline and selects the Action to use.
 
     Initializes all of the attributes described in the class docstring.
@@ -1284,6 +1372,7 @@
       raw_input_fn: Function used for getting user email.
       password_input_fn: Function used for getting user password.
       error_fh: Unexpected HTTPErrors are printed to this file handle.
+      update_check_class: UpdateCheck class (can be replaced for testing).
     """
     self.parser_class = parser_class
     self.argv = argv
@@ -1291,6 +1380,7 @@
     self.raw_input_fn = raw_input_fn
     self.password_input_fn = password_input_fn
     self.error_fh = error_fh
+    self.update_check_class = update_check_class
 
     self.parser = self._GetOptionParser()
     for action in self.actions.itervalues():
@@ -1571,7 +1661,7 @@
     appyaml = self._ParseAppYaml(basepath)
     rpc_server = self._GetRpcServer()
 
-    updatecheck = UpdateCheck(rpc_server, appyaml)
+    updatecheck = self.update_check_class(rpc_server, appyaml)
     updatecheck.CheckForUpdates()
 
     appversion = AppVersionUpload(rpc_server, appyaml)
@@ -1603,7 +1693,7 @@
       parser: An instance of OptionsParser.
     """
     parser.add_option("-S", "--max_size", type="int", dest="max_size",
-                      default=1048576, metavar="SIZE",
+                      default=10485760, metavar="SIZE",
                       help="Maximum size of a file to upload.")
 
   def VacuumIndexes(self):