186 version = yaml.safe_load(version_fh) |
190 version = yaml.safe_load(version_fh) |
187 finally: |
191 finally: |
188 version_fh.close() |
192 version_fh.close() |
189 |
193 |
190 return version |
194 return version |
|
195 |
|
196 def RetryWithBackoff(initial_delay, backoff_factor, max_tries, callable): |
|
197 """Calls a function multiple times, backing off more and more each time. |
|
198 |
|
199 Args: |
|
200 initial_delay: Initial delay after first try, in seconds. |
|
201 backoff_factor: Delay will be multiplied by this factor after each try. |
|
202 max_tries: Maximum number of tries. |
|
203 callable: The method to call, will pass no arguments. |
|
204 |
|
205 Returns: |
|
206 True if the function succeded in one of its tries. |
|
207 |
|
208 Raises: |
|
209 Whatever the function raises--an exception will immediately stop retries. |
|
210 """ |
|
211 delay = initial_delay |
|
212 while not callable() and max_tries > 0: |
|
213 StatusUpdate("Will check again in %s seconds." % delay) |
|
214 time.sleep(delay) |
|
215 delay *= backoff_factor |
|
216 max_tries -= 1 |
|
217 return max_tries > 0 |
191 |
218 |
192 |
219 |
193 class UpdateCheck(object): |
220 class UpdateCheck(object): |
194 """Determines if the local SDK is the latest version. |
221 """Determines if the local SDK is the latest version. |
195 |
222 |
911 version: The version string from 'config'. |
938 version: The version string from 'config'. |
912 files: A dictionary of files to upload to the server, mapping path to |
939 files: A dictionary of files to upload to the server, mapping path to |
913 hash of the file contents. |
940 hash of the file contents. |
914 in_transaction: True iff a transaction with the server has started. |
941 in_transaction: True iff a transaction with the server has started. |
915 An AppVersionUpload can do only one transaction at a time. |
942 An AppVersionUpload can do only one transaction at a time. |
|
943 deployed: True iff the Deploy method has been called. |
916 """ |
944 """ |
917 |
945 |
918 def __init__(self, server, config): |
946 def __init__(self, server, config): |
919 """Creates a new AppVersionUpload. |
947 """Creates a new AppVersionUpload. |
920 |
948 |
1057 """Commits the transaction, making the new app version available. |
1086 """Commits the transaction, making the new app version available. |
1058 |
1087 |
1059 All the files returned by Begin() must have been uploaded with UploadFile() |
1088 All the files returned by Begin() must have been uploaded with UploadFile() |
1060 before Commit() can be called. |
1089 before Commit() can be called. |
1061 |
1090 |
|
1091 This tries the new 'deploy' method; if that fails it uses the old 'commit'. |
|
1092 |
1062 Raises: |
1093 Raises: |
1063 Exception: Some required files were not uploaded. |
1094 Exception: Some required files were not uploaded. |
1064 """ |
1095 """ |
1065 assert self.in_transaction, "Begin() must be called before Commit()." |
1096 assert self.in_transaction, "Begin() must be called before Commit()." |
1066 if self.files: |
1097 if self.files: |
1067 raise Exception("Not all required files have been uploaded.") |
1098 raise Exception("Not all required files have been uploaded.") |
1068 |
1099 |
1069 StatusUpdate("Closing update.") |
1100 try: |
1070 self.server.Send("/api/appversion/commit", app_id=self.app_id, |
1101 self.Deploy() |
|
1102 if not RetryWithBackoff(1, 2, 8, self.IsReady): |
|
1103 logging.warning("Version still not ready to serve, aborting.") |
|
1104 raise Exception("Version not ready.") |
|
1105 self.StartServing() |
|
1106 except urllib2.HTTPError, e: |
|
1107 if e.code != 404: |
|
1108 raise |
|
1109 StatusUpdate("Closing update.") |
|
1110 self.server.Send("/api/appversion/commit", app_id=self.app_id, |
|
1111 version=self.version) |
|
1112 self.in_transaction = False |
|
1113 |
|
1114 def Deploy(self): |
|
1115 """Deploys the new app version but does not make it default. |
|
1116 |
|
1117 All the files returned by Begin() must have been uploaded with UploadFile() |
|
1118 before Deploy() can be called. |
|
1119 |
|
1120 Raises: |
|
1121 Exception: Some required files were not uploaded. |
|
1122 """ |
|
1123 assert self.in_transaction, "Begin() must be called before Deploy()." |
|
1124 if self.files: |
|
1125 raise Exception("Not all required files have been uploaded.") |
|
1126 |
|
1127 StatusUpdate("Deploying new version.") |
|
1128 self.server.Send("/api/appversion/deploy", app_id=self.app_id, |
1071 version=self.version) |
1129 version=self.version) |
1072 self.in_transaction = False |
1130 self.deployed = True |
|
1131 |
|
1132 def IsReady(self): |
|
1133 """Check if the new app version is ready to serve traffic. |
|
1134 |
|
1135 Raises: |
|
1136 Exception: Deploy has not yet been called. |
|
1137 |
|
1138 Returns: |
|
1139 True if the server returned the app is ready to serve. |
|
1140 """ |
|
1141 assert self.deployed, "Deploy() must be called before IsReady()." |
|
1142 |
|
1143 StatusUpdate("Checking if new version is ready to serve.") |
|
1144 result = self.server.Send("/api/appversion/isready", app_id=self.app_id, |
|
1145 version=self.version) |
|
1146 return result == "1" |
|
1147 |
|
1148 def StartServing(self): |
|
1149 """Start serving with the newly created version. |
|
1150 |
|
1151 Raises: |
|
1152 Exception: Deploy has not yet been called. |
|
1153 """ |
|
1154 assert self.deployed, "Deploy() must be called before IsReady()." |
|
1155 |
|
1156 StatusUpdate("Closing update: new version is ready to start serving.") |
|
1157 self.server.Send("/api/appversion/startserving", |
|
1158 app_id=self.app_id, version=self.version) |
1073 |
1159 |
1074 def Rollback(self): |
1160 def Rollback(self): |
1075 """Rolls back the transaction if one is in progress.""" |
1161 """Rolls back the transaction if one is in progress.""" |
1076 if not self.in_transaction: |
1162 if not self.in_transaction: |
1077 return |
1163 return |
1269 |
1356 |
1270 def __init__(self, argv, parser_class=optparse.OptionParser, |
1357 def __init__(self, argv, parser_class=optparse.OptionParser, |
1271 rpc_server_class=appengine_rpc.HttpRpcServer, |
1358 rpc_server_class=appengine_rpc.HttpRpcServer, |
1272 raw_input_fn=raw_input, |
1359 raw_input_fn=raw_input, |
1273 password_input_fn=getpass.getpass, |
1360 password_input_fn=getpass.getpass, |
1274 error_fh=sys.stderr): |
1361 error_fh=sys.stderr, |
|
1362 update_check_class=UpdateCheck): |
1275 """Initializer. Parses the cmdline and selects the Action to use. |
1363 """Initializer. Parses the cmdline and selects the Action to use. |
1276 |
1364 |
1277 Initializes all of the attributes described in the class docstring. |
1365 Initializes all of the attributes described in the class docstring. |
1278 Prints help or error messages if there is an error parsing the cmdline. |
1366 Prints help or error messages if there is an error parsing the cmdline. |
1279 |
1367 |
1282 parser_class: Options parser to use for this application. |
1370 parser_class: Options parser to use for this application. |
1283 rpc_server_class: RPC server class to use for this application. |
1371 rpc_server_class: RPC server class to use for this application. |
1284 raw_input_fn: Function used for getting user email. |
1372 raw_input_fn: Function used for getting user email. |
1285 password_input_fn: Function used for getting user password. |
1373 password_input_fn: Function used for getting user password. |
1286 error_fh: Unexpected HTTPErrors are printed to this file handle. |
1374 error_fh: Unexpected HTTPErrors are printed to this file handle. |
|
1375 update_check_class: UpdateCheck class (can be replaced for testing). |
1287 """ |
1376 """ |
1288 self.parser_class = parser_class |
1377 self.parser_class = parser_class |
1289 self.argv = argv |
1378 self.argv = argv |
1290 self.rpc_server_class = rpc_server_class |
1379 self.rpc_server_class = rpc_server_class |
1291 self.raw_input_fn = raw_input_fn |
1380 self.raw_input_fn = raw_input_fn |
1292 self.password_input_fn = password_input_fn |
1381 self.password_input_fn = password_input_fn |
1293 self.error_fh = error_fh |
1382 self.error_fh = error_fh |
|
1383 self.update_check_class = update_check_class |
1294 |
1384 |
1295 self.parser = self._GetOptionParser() |
1385 self.parser = self._GetOptionParser() |
1296 for action in self.actions.itervalues(): |
1386 for action in self.actions.itervalues(): |
1297 action.options(self, self.parser) |
1387 action.options(self, self.parser) |
1298 |
1388 |
1569 |
1659 |
1570 basepath = self.args[0] |
1660 basepath = self.args[0] |
1571 appyaml = self._ParseAppYaml(basepath) |
1661 appyaml = self._ParseAppYaml(basepath) |
1572 rpc_server = self._GetRpcServer() |
1662 rpc_server = self._GetRpcServer() |
1573 |
1663 |
1574 updatecheck = UpdateCheck(rpc_server, appyaml) |
1664 updatecheck = self.update_check_class(rpc_server, appyaml) |
1575 updatecheck.CheckForUpdates() |
1665 updatecheck.CheckForUpdates() |
1576 |
1666 |
1577 appversion = AppVersionUpload(rpc_server, appyaml) |
1667 appversion = AppVersionUpload(rpc_server, appyaml) |
1578 appversion.DoUpload(FileIterator(basepath), self.options.max_size, |
1668 appversion.DoUpload(FileIterator(basepath), self.options.max_size, |
1579 lambda path: open(os.path.join(basepath, path), "rb")) |
1669 lambda path: open(os.path.join(basepath, path), "rb")) |
1601 |
1691 |
1602 Args: |
1692 Args: |
1603 parser: An instance of OptionsParser. |
1693 parser: An instance of OptionsParser. |
1604 """ |
1694 """ |
1605 parser.add_option("-S", "--max_size", type="int", dest="max_size", |
1695 parser.add_option("-S", "--max_size", type="int", dest="max_size", |
1606 default=1048576, metavar="SIZE", |
1696 default=10485760, metavar="SIZE", |
1607 help="Maximum size of a file to upload.") |
1697 help="Maximum size of a file to upload.") |
1608 |
1698 |
1609 def VacuumIndexes(self): |
1699 def VacuumIndexes(self): |
1610 """Deletes unused indexes.""" |
1700 """Deletes unused indexes.""" |
1611 if len(self.args) != 1: |
1701 if len(self.args) != 1: |