OLD | NEW |
(Empty) | |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. |
| 4 |
| 5 """Provides a model to support versioned entities in datastore. |
| 6 |
| 7 Idea: use a root model entity to keep track of the most recent version of a |
| 8 versioned entity, and make the versioned entities and the root model entity in |
| 9 the same entity group so that they could be read and written in a transaction. |
| 10 """ |
| 11 |
| 12 import logging |
| 13 |
| 14 from google.appengine.api import datastore_errors |
| 15 from google.appengine.ext import ndb |
| 16 from google.appengine.runtime import apiproxy_errors |
| 17 |
| 18 |
| 19 class _GroupRoot(ndb.Model): |
| 20 """Root entity of a group to support versioned children.""" |
| 21 # Key id of the most recent child entity in the datastore. It is monotonically |
| 22 # increasing and is 0 if no child is present. |
| 23 current = ndb.IntegerProperty(indexed=False, default=0) |
| 24 |
| 25 |
| 26 class VersionedModel(ndb.Model): |
| 27 """A model that supports versioning. |
| 28 |
| 29 Subclass will automatically be versioned, if use GetMostRecentVersion() to |
| 30 read and use Save() to write. |
| 31 """ |
| 32 |
| 33 @property |
| 34 def version(self): |
| 35 return self.key.integer_id() |
| 36 |
| 37 @classmethod |
| 38 def GetMostRecentVersion(cls): |
| 39 """Returns the most recent version of the entity.""" |
| 40 assert not ndb.in_transaction() |
| 41 |
| 42 root_key = cls._GetRootKey() |
| 43 root = root_key.get() |
| 44 if not root or not root.current: |
| 45 return None |
| 46 |
| 47 return ndb.Key(cls, root.current, parent=root_key).get() |
| 48 |
| 49 def Save(self): |
| 50 """Saves the current entity, but as a new version.""" |
| 51 root_key = self._GetRootKey() |
| 52 root = root_key.get() or self._GetRootModel()(key=root_key) |
| 53 |
| 54 def SaveData(): |
| 55 if self.key.get(): |
| 56 return False # The entity exists, should retry. |
| 57 ndb.put_multi([self, root]) |
| 58 return True |
| 59 |
| 60 def SetNewKey(): |
| 61 root.current += 1 |
| 62 self.key = ndb.Key(self.__class__, root.current, parent=root_key) |
| 63 |
| 64 SetNewKey() |
| 65 while True: |
| 66 while self.key.get(): |
| 67 SetNewKey() |
| 68 |
| 69 try: |
| 70 if ndb.transaction(SaveData, retries=0): |
| 71 return self.key |
| 72 except ( |
| 73 datastore_errors.InternalError, |
| 74 datastore_errors.Timeout, |
| 75 datastore_errors.TransactionFailedError) as e: |
| 76 # https://cloud.google.com/appengine/docs/python/datastore/transactions |
| 77 # states the result is ambiguous, it could have succeeded. |
| 78 logging.info('Transaction likely failed: %s', e) |
| 79 except ( |
| 80 apiproxy_errors.CancelledError, |
| 81 datastore_errors.BadRequestError, |
| 82 RuntimeError) as e: |
| 83 logging.info('Transaction failure: %s', e) |
| 84 else: |
| 85 SetNewKey() |
| 86 |
| 87 @classmethod |
| 88 def _GetRootModel(cls): |
| 89 """Returns a root model that can be used for versioned entities.""" |
| 90 root_model_name = '%sRoot' % cls.__name__ |
| 91 |
| 92 class _RootModel(_GroupRoot): |
| 93 @classmethod |
| 94 def _get_kind(cls): |
| 95 return root_model_name |
| 96 |
| 97 return _RootModel |
| 98 |
| 99 @classmethod |
| 100 def _GetRootKey(cls): |
| 101 return ndb.Key(cls._GetRootModel(), 1) |
OLD | NEW |