代码之家  ›  专栏  ›  技术社区  ›  David Underhill

应用程序引擎上的故障保护数据存储更新

  •  2
  • David Underhill  · 技术社区  · 14 年前

    当然,应用引擎数据存储 downtime . 但是,我想要一个“故障保险箱” put 在数据存储错误面前,它更为强大(见下面的动机)。当数据存储不可用时,任务队列似乎是延迟写入的明显位置。不过,我不知道其他任何解决方案(除了通过urlfetch将数据发送给第三方之外)。

    动机 :我有一个实体 真正地 需要放在数据存储中-只向用户显示一条错误消息是不行的。例如,可能发生了一些不容易消除的副作用(可能与第三方站点进行了一些交互)。

    我想出了一个简单的包装器,它(我认为)提供了一个合理的“故障安全”放置(见下文)。您是否看到了这方面的任何问题,或者对更健壮的实现有了想法?(注:感谢尼克·约翰逊和萨克森·德鲁斯在回答中提出的建议,本文经过了编辑,对代码进行了一些改进。)

    import logging
    from google.appengine.api.labs.taskqueue import taskqueue
    from google.appengine.datastore import entity_pb
    from google.appengine.ext import db
    from google.appengine.runtime.apiproxy_errors import CapabilityDisabledError
    
    def put_failsafe(e, db_put_deadline=20, retry_countdown=60, queue_name='default'):
        """Tries to e.put().  On success, 1 is returned.  If this raises a db.Error
        or CapabilityDisabledError, then a task will be enqueued to try to put the
        entity (the task will execute after retry_countdown seconds) and 2 will be
        returned.  If the task cannot be enqueued, then 0 will be returned.  Thus a
        falsey value is only returned on complete failure.
    
        Note that since the taskqueue payloads are limited to 10kB, if the protobuf
        representing e is larger than 10kB then the put will be unable to be
        deferred to the taskqueue.
    
        If a put is deferred to the taskqueue, then it won't necessarily be
        completed as soon as the datastore is back up.  Thus it is possible that
        e.put() will occur *after* other, later puts when 1 is returned.
    
        Ensure e's model is imported in the code which defines the task which tries
        to re-put e (so that e can be deserialized).
        """
        try:
            e.put(rpc=db.create_rpc(deadline=db_put_deadline))
            return 1
        except (db.Error, CapabilityDisabledError), ex1:
            try:
                taskqueue.add(queue_name=queue_name,
                              countdown=retry_countdown,
                              url='/task/retry_put',
                              payload=db.model_to_protobuf(e).Encode())
                logging.info('failed to put to db now, but deferred put to the taskqueue e=%s ex=%s' % (e, ex1))
                return 2
            except (taskqueue.Error, CapabilityDisabledError), ex2:
                return 0
    

    任务的请求处理程序:

    from google.appengine.ext import db, webapp
    
    # IMPORTANT: This task deserializes entity protobufs.  To ensure that this is
    #            successful, you must import any db.Model that may need to be
    #            deserialized here (otherwise this task may raise a KindError).
    
    class RetryPut(webapp.RequestHandler):
        def post(self):
            e = db.model_from_protobuf(entity_pb.EntityProto(self.request.body))
            e.put() # failure will raise an exception => the task to be retried
    

    不要 希望将其用于 每一个 输入-大多数情况下,显示错误消息是很好的。对每个Put都使用它是很有诱惑力的,但我认为有时如果我告诉用户更改将在稍后出现(并继续向他们显示旧数据,直到数据存储被备份为止),用户可能会更加困惑 延期执行)。

    2 回复  |  直到 14 年前
        1
  •  2
  •   Nick Johnson    14 年前

    您的方法是合理的,但有几个注意事项:

    • 默认情况下,Put操作将重试,直到超时为止。由于您有备份策略,您可能希望尽早放弃—在这种情况下,您应该为put方法调用提供一个rpc参数,指定一个自定义的截止时间。
    • 不需要设置显式倒计时-任务队列将每隔一段时间为您重试失败的操作。
    • 您不需要使用pickle-协议缓冲区有一个自然的字符串编码,它效率更高。见 this post 演示如何使用它。
    • 正如Saxon指出的那样,任务队列有效负载被限制为10千字节,因此您可能会遇到大型实体的问题。
    • 最重要的是,这会将数据存储一致性模型从“强一致性”更改为“最终一致性”。也就是说,您排队进入任务队列的Put可以在将来的任何时候应用,覆盖在临时中所做的任何更改。任何数量的争用条件都是可能的,如果任务队列中存在挂起的放置,则实质上会使事务变得无用。
        2
  •  1
  •   Saxon Druce    14 年前

    一个潜在的问题是 tasks are limited to 10kb of data 因此,如果有一个实体大于曾经被腌制过的实体,那么这将不起作用。