Suppress duplicate messages from a client (e.g. when network failure causes retries for RPCs that actually went through).
This commit is contained in:
10
api.py
10
api.py
@@ -53,9 +53,17 @@ def CreateChannel(google_user, client, args):
|
|||||||
def SendMessage(google_user, client, args):
|
def SendMessage(google_user, client, args):
|
||||||
subject = args['subject']
|
subject = args['subject']
|
||||||
message = args['message']
|
message = args['message']
|
||||||
|
sender_message_id = args['sender_message_id']
|
||||||
key = args.get('key', None)
|
key = args.get('key', None)
|
||||||
|
|
||||||
models.Subject.FindOrCreate(subject).SendMessage(message, client.parent_key(), key)
|
try:
|
||||||
|
models.Subject.FindOrCreate(subject).SendMessage(
|
||||||
|
message, client.parent_key(), sender_message_id, key)
|
||||||
|
except models.DuplicateMessage:
|
||||||
|
logging.exception('Duplicate message: %s', sender_message_id)
|
||||||
|
# We still return success since we assume that the message was already
|
||||||
|
# delivered. If it's really a client ID generation bug, we just swallowed
|
||||||
|
# a message.
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,10 @@ import utils
|
|||||||
# ↳ Subscription (⤴︎ Client)
|
# ↳ Subscription (⤴︎ Client)
|
||||||
|
|
||||||
|
|
||||||
|
class DuplicateMessage(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class Profile(db.Model):
|
class Profile(db.Model):
|
||||||
google_user = db.UserProperty()
|
google_user = db.UserProperty()
|
||||||
|
|
||||||
@@ -119,14 +123,29 @@ class Subject(db.Model):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
@db.transactional()
|
@db.transactional()
|
||||||
def PutMessage(self, message, sender, key=None):
|
def PutMessage(self, message, sender, sender_message_id, key=None):
|
||||||
"""Internal helper for SendMessage().
|
"""Internal helper for SendMessage().
|
||||||
|
|
||||||
Unless/until channel.send_message becomes transactional, we have to finish
|
Unless/until channel.send_message becomes transactional, we have to finish
|
||||||
the datastore work (and any retries) before we start transmitting to
|
the datastore work (and any retries) before we start transmitting to
|
||||||
channels.
|
channels.
|
||||||
"""
|
"""
|
||||||
obj = Message(parent=self, message=message, sender=sender, key_=key)
|
# sender_message_id should be universal across all subjects, but we check
|
||||||
|
# it within just this subject to allow in-transaction verification.
|
||||||
|
messages = (
|
||||||
|
Message.all()
|
||||||
|
.ancestor(self)
|
||||||
|
.filter('sender_message_id =', sender_message_id)
|
||||||
|
.fetch(1))
|
||||||
|
if messages:
|
||||||
|
raise DuplicateMessage(sender_message_id)
|
||||||
|
|
||||||
|
obj = Message(
|
||||||
|
parent=self,
|
||||||
|
message=message,
|
||||||
|
sender=sender,
|
||||||
|
sender_message_id=sender_message_id,
|
||||||
|
key_=key)
|
||||||
obj.put()
|
obj.put()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -134,8 +153,8 @@ class Subject(db.Model):
|
|||||||
[Subscription.client.get_value_for_datastore(subscription)
|
[Subscription.client.get_value_for_datastore(subscription)
|
||||||
for subscription in Subscription.all().ancestor(self)])
|
for subscription in Subscription.all().ancestor(self)])
|
||||||
|
|
||||||
def SendMessage(self, message, sender, key=None):
|
def SendMessage(self, message, sender, sender_message_id, key=None):
|
||||||
obj, subscriptions = self.PutMessage(message, sender, key)
|
obj, subscriptions = self.PutMessage(message, sender, sender_message_id, key)
|
||||||
event = obj.ToEvent()
|
event = obj.ToEvent()
|
||||||
for subscription in subscriptions:
|
for subscription in subscriptions:
|
||||||
Client.SendByKey(subscription, event)
|
Client.SendByKey(subscription, event)
|
||||||
@@ -177,6 +196,7 @@ class Message(db.Model):
|
|||||||
created = db.DateTimeProperty(required=True, auto_now_add=True)
|
created = db.DateTimeProperty(required=True, auto_now_add=True)
|
||||||
message = db.TextProperty(required=True)
|
message = db.TextProperty(required=True)
|
||||||
sender = db.ReferenceProperty(required=True, reference_class=Profile)
|
sender = db.ReferenceProperty(required=True, reference_class=Profile)
|
||||||
|
sender_message_id = db.StringProperty(required=True)
|
||||||
# key and key_name are reserved
|
# key and key_name are reserved
|
||||||
key_ = db.StringProperty()
|
key_ = db.StringProperty()
|
||||||
|
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ Cosmopolite.prototype.sendMessage = function(subject, message, key) {
|
|||||||
var args = {
|
var args = {
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
'message': JSON.stringify(message),
|
'message': JSON.stringify(message),
|
||||||
|
'sender_message_id': this.uuid_(),
|
||||||
};
|
};
|
||||||
if (key) {
|
if (key) {
|
||||||
args['key'] = key;
|
args['key'] = key;
|
||||||
@@ -189,6 +190,23 @@ Cosmopolite.prototype.loggingPrefix_ = function() {
|
|||||||
return 'cosmopolite (' + this.namespace_ + '):';
|
return 'cosmopolite (' + this.namespace_ + '):';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a v4 UUID.
|
||||||
|
*
|
||||||
|
* @return {string} A universally-unique random value.
|
||||||
|
* @const
|
||||||
|
*/
|
||||||
|
Cosmopolite.prototype.uuid_ = function() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||||
|
var r = (Math.random() * 16) | 0;
|
||||||
|
if (c == 'x') {
|
||||||
|
return r.toString(16);
|
||||||
|
} else {
|
||||||
|
return (r & (0x03 | 0x08)).toString(16);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Callback when a script loads.
|
* Callback when a script loads.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user