From cf05c0f62061ef6bbd1b3effd2d4de183a3a7efe Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 6 May 2014 13:38:40 -0700 Subject: [PATCH] Switch to a multi-request/response API to make batching possible. --- api.py | 128 ++++++++++++++++++++++-------------------- include.yaml | 2 +- lib/security.py | 2 +- lib/session.py | 4 +- lib/utils.py | 10 ++++ static/cosmopolite.js | 61 +++++++++++--------- 6 files changed, 116 insertions(+), 91 deletions(-) diff --git a/api.py b/api.py index 981f88d..f4dc0ff 100644 --- a/api.py +++ b/api.py @@ -26,74 +26,82 @@ from cosmopolite.lib import utils import config -class SetValue(webapp2.RequestHandler): - @utils.chaos_monkey - @utils.returns_json - @utils.local_namespace - @security.google_user_xsrf_protection - @security.weak_security_checks - @session.session_required - @db.transactional() - def post(self): - entry_key = self.request.get('key') - entry_value = self.request.get('value') - public = (self.request.get('public') == 'true') - - entries = (models.StateEntry.all() - .ancestor(self.client.parent_key()) - .filter('entry_key =', entry_key) - .fetch(1)) - if entries: - entry = entries[0] - entry.entry_value = entry_value - entry.public = public - else: - entry = models.StateEntry( - parent=self.client.parent_key(), - entry_key=entry_key, - entry_value=entry_value, - public=public) - - entry.put() - msg = entry.ToMessage() - clients = (models.Client.all() - .ancestor(self.client.parent_key())) - for client in clients: - client.SendMessage(msg) - - return {} - - -class CreateChannel(webapp2.RequestHandler): +@db.transactional() +def SetValue(google_user, client, args): + entry_key = args['key'] + entry_value = args['value'] + public = (args['public'] == 'true') + + entries = (models.StateEntry.all() + .ancestor(client.parent_key()) + .filter('entry_key =', entry_key) + .fetch(1)) + if entries: + entry = entries[0] + entry.entry_value = entry_value + entry.public = public + else: + entry = models.StateEntry( + parent=client.parent_key(), + entry_key=entry_key, + entry_value=entry_value, + public=public) + + entry.put() + msg = entry.ToMessage() + clients = (models.Client.all() + .ancestor(client.parent_key())) + for client in clients: + client.SendMessage(msg) + + return {} + + +def CreateChannel(google_user, client, args): + token = channel.create_channel( + client_id=str(client.key()), + duration_minutes=config.CHANNEL_DURATION_SECONDS / 60) + messages = [x.ToMessage() + for x in client.parent().GetStateEntries()] + if google_user: + messages.append({ + 'message_type': 'login', + 'google_user': google_user.email(), + }) + else: + messages.append({ + 'message_type': 'logout', + }) + + return { + 'token': token, + 'messages': messages, + } + + +class APIWrapper(webapp2.RequestHandler): + + _COMMANDS = { + 'createChannel': CreateChannel, + 'setValue': SetValue, + } + @utils.chaos_monkey + @utils.expects_json @utils.returns_json @utils.local_namespace @security.google_user_xsrf_protection @security.weak_security_checks @session.session_required def post(self): - token = channel.create_channel( - client_id=str(self.client.key()), - duration_minutes=config.CHANNEL_DURATION_SECONDS / 60) - messages = [x.ToMessage() - for x in self.client.parent().GetStateEntries()] - if self.verified_google_user: - messages.append({ - 'message_type': 'login', - 'google_user': self.verified_google_user.email(), - }) - else: - messages.append({ - 'message_type': 'logout', - }) - - return { - 'token': token, - 'messages': messages, - } + ret = [] + for command in self.request_json['commands']: + callback = self._COMMANDS[command['command']] + result = callback(self.verified_google_user, self.client, command.get('arguments', {})) + ret.append(result) + return ret app = webapp2.WSGIApplication([ - (config.URL_PREFIX + '/api/createChannel', CreateChannel), - (config.URL_PREFIX + '/api/setValue', SetValue), + (config.URL_PREFIX + '/api', APIWrapper), ]) diff --git a/include.yaml b/include.yaml index b06714e..55edd70 100644 --- a/include.yaml +++ b/include.yaml @@ -1,5 +1,5 @@ handlers: -- url: /cosmopolite/api/.* +- url: /cosmopolite/api script: cosmopolite.api.app secure: always diff --git a/lib/security.py b/lib/security.py index 7e2da53..f84252b 100644 --- a/lib/security.py +++ b/lib/security.py @@ -36,7 +36,7 @@ def google_user_xsrf_protection(handler): if not google_user: return handler(self) - google_user_id = auth.Parse(self.request.get('google_user_id', None)) + google_user_id = auth.Parse(self.request_json.get('google_user_id', None)) if (not google_user_id or google_user_id != google_user.user_id()): return { diff --git a/lib/session.py b/lib/session.py index 31e5eb4..75e5881 100644 --- a/lib/session.py +++ b/lib/session.py @@ -65,7 +65,7 @@ def session_required(handler): @functools.wraps(handler) def FindOrCreateSession(self): - client_key = auth.ParseKey(self.request.get('client_id', None)) + client_key = auth.ParseKey(self.request_json.get('client_id', None)) # The hunt for a Profile begins. if client_key: @@ -77,7 +77,7 @@ def session_required(handler): ret = { 'status': 'ok', - 'response': handler(self), + 'responses': handler(self), } if client_key != self.client.key(): # Tell the client that this changed diff --git a/lib/utils.py b/lib/utils.py index 43e6502..58b00f6 100644 --- a/lib/utils.py +++ b/lib/utils.py @@ -25,6 +25,16 @@ from cosmopolite import config from cosmopolite.lib import auth +def expects_json(handler): + + @functools.wraps(handler) + def ParseInput(self): + self.request_json = json.load(self.request.body_file) + return handler(self) + + return ParseInput + + def returns_json(handler): @functools.wraps(handler) diff --git a/static/cosmopolite.js b/static/cosmopolite.js index 3e286e2..26e3d84 100644 --- a/static/cosmopolite.js +++ b/static/cosmopolite.js @@ -75,17 +75,39 @@ cosmopolite.Client.prototype.registerMessageHandlers_ = function() { }, this)); }; -cosmopolite.Client.prototype.sendRPC_ = function(command, data, onSuccess, delay) { +cosmopolite.Client.prototype.sendRPC_ = function(command, args, onSuccess) { + this.sendRPCs_([ + { + 'command': command, + 'arguments': args, + 'onSuccess': onSuccess, + } + ]); +}; + +cosmopolite.Client.prototype.sendRPCs_ = function(commands, delay) { + var request = { + 'commands': [], + }; + commands.forEach(function(command) { + var request_command = { + 'command': command['command'], + }; + if ('arguments' in command) { + request_command['arguments'] = command['arguments']; + } + request.commands.push(request_command); + }); if (this.namespace_ + ':client_id' in localStorage) { - data['client_id'] = localStorage[this.namespace_ + ':client_id']; + request['client_id'] = localStorage[this.namespace_ + ':client_id']; } if (this.namespace_ + ':google_user_id' in localStorage) { - data['google_user_id'] = localStorage[this.namespace_ + ':google_user_id']; + request['google_user_id'] = localStorage[this.namespace_ + ':google_user_id']; } this.$.ajax({ - url: this.urlPrefix_ + '/api/' + command, + url: this.urlPrefix_ + '/api', type: 'post', - data: data, + data: JSON.stringify(request), dataType: 'json', context: this, }) @@ -99,7 +121,7 @@ cosmopolite.Client.prototype.sendRPC_ = function(command, data, onSuccess, delay } if (data['status'] == 'retry') { // Discard delay - this.sendRPC_(command, data, onSuccess); + this.sendRPCs_(commands, onSuccess); return; } if (data['status'] != 'ok') { @@ -109,8 +131,10 @@ cosmopolite.Client.prototype.sendRPC_ = function(command, data, onSuccess, delay // TODO(flamingcow): Refresh the page? Show an alert? return; } - if (onSuccess) { - this.$.proxy(onSuccess, this)(data.response); + for (var i = 0; i < data.responses.length; i++) { + if (commands[i]['onSuccess']) { + this.$.proxy(commands[i]['onSuccess'], this)(data.responses[i]); + } } }) .fail(function(xhr) { @@ -118,31 +142,14 @@ cosmopolite.Client.prototype.sendRPC_ = function(command, data, onSuccess, delay xhr.getResponseHeader('Retry-After') || Math.min(32, Math.max(2, delay || 2)); console.log( - 'RPC ' + command + ' failed. Will retry in ' + intDelay + ' seconds'); + 'RPC failed. Will retry in ' + intDelay + ' seconds'); function retry() { - this.sendRPC_(command, data, onSuccess, Math.pow(intDelay, 2)); + this.sendRPCs_(commands, Math.pow(intDelay, 2)); } window.setTimeout(this.$.proxy(retry, this), intDelay * 1000); }); }; -cosmopolite.Client.prototype.getUser_ = function() { - this.sendRPC_('getUser', {}, function(data) { - if ('google_user' in data) { - if ('onLogin' in this.callbacks_) { - this.callbacks_['onLogin']( - data['google_user'], - this.urlPrefix_ + '/auth/logout'); - } - } else { - if ('onLogout' in this.callbacks_) { - this.callbacks_['onLogout']( - this.urlPrefix_ + '/auth/login'); - } - } - }); -}; - cosmopolite.Client.prototype.setValue = function(key, value, is_public) { this.sendRPC_('setValue', { 'key': key,