Chromium Code Reviews| Index: appengine/findit/model/versioned_model.py |
| diff --git a/appengine/findit/model/versioned_model.py b/appengine/findit/model/versioned_model.py |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..b59b90998a5deec697c4d3a6d7f449b4d1698464 |
| --- /dev/null |
| +++ b/appengine/findit/model/versioned_model.py |
| @@ -0,0 +1,101 @@ |
| +# Copyright 2015 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
| +"""Provides a model to support versioned entities in datastore. |
| + |
| +Idea: use a root model entity to keep track of the most recent version of a |
| +versioned entity, and make the versioned entities and the root model entity in |
| +the same entity group so that they could be read and written in a transaction. |
| +""" |
| + |
| +import logging |
| + |
| +from google.appengine.api import datastore_errors |
| +from google.appengine.ext import ndb |
| +from google.appengine.runtime import apiproxy_errors |
| + |
| + |
| +class _GroupRoot(ndb.Model): |
| + """Root entity of a group to support versioned children.""" |
| + # Key id of the most recent child entity in the datastore. It is monotonically |
| + # increasing and is 0 if no child is present. |
| + current = ndb.IntegerProperty(indexed=False, default=0) |
| + |
| + |
| +class VersionedModel(ndb.Model): |
| + """A model that supports versioning. |
| + |
| + Subclass will automatically be versioned, if use GetMostRecentVersion() to |
| + read and use Save() to write. |
| + """ |
| + |
| + @property |
| + def version(self): |
| + return self.key.integer_id() |
| + |
| + @classmethod |
| + def GetMostRecentVersion(cls): |
| + """Returns the most recent version of the entity.""" |
| + assert not ndb.in_transaction() |
| + |
| + root_key = cls._GetRootKey() |
| + root = root_key.get() |
| + if not root or not root.current: |
| + return None |
| + |
| + return ndb.Key(cls, root.current, parent=root_key).get() |
| + |
| + def Save(self): |
| + """Saves the current entity, but as a new version.""" |
| + root_key = self._GetRootKey() |
| + root = root_key.get() or self._GetRootModel()(key=root_key) |
| + |
| + def SaveData(): |
| + if self.key.get(): |
| + return False # The entity exists, should retry. |
| + ndb.put_multi([self, root]) |
| + return True |
| + |
| + def SetNewKey(): |
| + root.current += 1 |
| + self.key = ndb.Key(self.__class__, root.current, parent=root_key) |
| + |
| + SetNewKey() |
| + while True: |
|
lijeffrey1
2015/12/03 18:51:37
so is this while True used to perform a retry if a
stgao
2015/12/03 19:43:56
Yes, this is to retry for exception or key already
|
| + while self.key.get(): |
| + SetNewKey() |
| + |
| + try: |
| + if ndb.transaction(SaveData, retries=0): |
| + return self.key |
| + except ( |
| + datastore_errors.InternalError, |
| + datastore_errors.Timeout, |
| + datastore_errors.TransactionFailedError) as e: |
| + # https://cloud.google.com/appengine/docs/python/datastore/transactions |
| + # states the result is ambiguous, it could have succeeded. |
| + logging.info('Transaction likely failed: %s', e) |
| + except ( |
| + apiproxy_errors.CancelledError, |
| + datastore_errors.BadRequestError, |
| + RuntimeError) as e: |
| + logging.info('Transaction failure: %s', e) |
| + else: |
| + SetNewKey() |
| + |
| + @classmethod |
| + def _GetRootModel(cls): |
| + """Returns a root model that can be used for versioned entities.""" |
| + root_model_name = '%sRoot' % cls.__name__ |
| + |
| + class _RootModel(_GroupRoot): |
| + @classmethod |
| + def _get_kind(cls): |
| + return root_model_name |
| + |
| + return _RootModel |
| + |
| + @classmethod |
| + def _GetRootKey(cls): |
| + return ndb.Key(cls._GetRootModel(), 1) |