我正在使用taskqueue API通过mailgun来发送多封电子邮件。我的代码大致如下所示:
class CpMsg(ndb.Model):
group = ndb.KeyProperty()
sent = ndb.BooleanProperty()
#Other properties
def send_mail(messages):
"""Sends a request to mailgun's API"""
# Some code
pass
class MailTask(TaskHandler):
def post(self):
p_key = utils.key_from_string(self.request.get('p'))
msgs = CpMsg.query(
CpMsg.group==p_key,
CpMsg.sent==False).fetch(BATCH_SIZE)
if msgs:
send_mail(msgs)
for msg in msgs:
msg.sent = True
ndb.put_multi(msgs)
#Call the task again in COOLDOWN seconds
上面的代码运行良好,但是根据文档,taskqueue API保证至少要一次 交付任务,因此任务应该是幂等的。现在,在大多数情况下,上面的代码就是这种情况,因为它只获取“已发送”属性等于False的消息。问题在于非祖先的ndb查询最终只会保持一致,这意味着如果快速连续执行两次任务,查询可能会返回陈旧的结果并包含刚刚发送的消息。
我曾考虑为消息添加一个祖先,但是由于发送的电子邮件将成千上万,因此我担心这可能意味着拥有大型实体组,而这些实体组的写吞吐量有限。
我应该使用祖先进行查询吗?也许有一种方法可以配置mailgun以避免两次发送相同的电子邮件?我应该接受这样的风险吗?在极少数情况下,可能会多次发送几封电子邮件?
答案 0 :(得分:1)
避免最终一致性障碍的一种可能方法是使查询成为keys_only
,然后遍历消息键以通过键查找(强一致性)获取实际消息,检查{{1} }为True,在这种情况下跳过发送这些消息。遵循以下原则:
msg.sent
您还必须使 msg_keys = CpMsg.query(
CpMsg.group==p_key,
CpMsg.sent==False).fetch(BATCH_SIZE, keys_only=True)
if not msg_keys:
return
msgs = ndb.get_multi(msg_keys)
msgs_to_send = []
for msg in msgs:
if not msg.sent:
msgs_to_send.append(msg)
if msgs_to_send:
send_mail(msgs_to_send)
for msg in msgs_to_send:
msg.sent = True
ndb.put_multi(msgs_to_send)
呼叫具有事务性(使用post
装饰器)。
这应该解决由查询最终一致性导致的重复项。但是,由于数据存储区争用(或任何其他原因),由于事务重试而导致的重复项仍有空间-因为@ndb.transactional()
调用不是幂等的。一次发送一条消息(也许使用任务队列)可以减少发生这种情况的机会。另请参见GAE/P: Transaction safety with API calls