| 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:
|
| + 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)
|
|
|