Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Provides a model to support versioned entities in datastore. | 5 """Provides a model to support versioned entities in datastore. |
| 6 | 6 |
| 7 Idea: use a root model entity to keep track of the most recent version of a | 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 | 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. | 9 the same entity group so that they could be read and written in a transaction. |
| 10 """ | 10 """ |
| 11 | 11 |
| 12 import logging | 12 import logging |
| 13 | 13 |
| 14 from google.appengine.api import datastore_errors | 14 from google.appengine.api import datastore_errors |
| 15 from google.appengine.ext import ndb | 15 from google.appengine.ext import ndb |
| 16 from google.appengine.runtime import apiproxy_errors | 16 from google.appengine.runtime import apiproxy_errors |
| 17 | 17 |
| 18 | 18 |
| 19 class _GroupRoot(ndb.Model): | 19 class _GroupRoot(ndb.Model): |
| 20 """Root entity of a group to support versioned children.""" | 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 | 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. | 22 # increasing and is 0 if no child is present. |
| 23 current = ndb.IntegerProperty(indexed=False, default=0) | 23 current = ndb.IntegerProperty(indexed=False, default=0) |
| 24 | 24 |
| 25 | 25 |
| 26 class VersionedModel(ndb.Model): | 26 class VersionedModel(ndb.Model): |
| 27 """A model that supports versioning. | 27 """A model that supports versioning. |
| 28 | 28 |
| 29 Subclass will automatically be versioned, if use GetVersion() to | 29 Subclass will automatically be versioned, if use GetVersion() to |
|
stgao
2016/09/24 01:43:12
Update this to reflect the addition of Create belo
lijeffrey
2016/09/24 06:13:14
Done.
| |
| 30 read and use Save() to write. | 30 read and use Save() to write. |
| 31 """ | 31 """ |
| 32 | 32 |
| 33 @property | 33 @property |
| 34 def _root_id(self): | |
| 35 return self.key.pairs()[0][1] if self.key else None | |
| 36 | |
| 37 @property | |
| 34 def version(self): | 38 def version(self): |
| 35 return self.key.integer_id() if self.key else 0 | 39 # Ndb treats key.integer_id() of 0 as None, so default to 0. |
| 40 return self.key.integer_id() or 0 if self.key else 0 | |
| 36 | 41 |
| 37 @classmethod | 42 @classmethod |
| 38 def GetVersion(cls, version=None): | 43 def Create(cls, root_id=None): |
| 44 """Creates an instance of cls that is to become the first version. | |
| 45 | |
| 46 The calling function of Create() should be responsible first for checking | |
| 47 no previous version of the proposed entity already exists. | |
| 48 | |
| 49 Args: | |
| 50 root_id: Any user-specified value that will serve as the id for the root | |
| 51 entity's key. | |
| 52 | |
| 53 Returns: | |
| 54 An instance of cls meant to be the first version. Note for this instance | |
| 55 to be committed to the datastore Save() would need to be called on the | |
| 56 instance returned by this method. | |
| 57 """ | |
| 58 return cls(key=ndb.Key(cls, 0, parent=cls._GetRootKey(root_id))) | |
| 59 | |
| 60 @classmethod | |
| 61 def GetVersion(cls, root_id=None, version=None): | |
| 39 """Returns a version of the entity, the latest if version=None.""" | 62 """Returns a version of the entity, the latest if version=None.""" |
| 40 assert not ndb.in_transaction() | 63 assert not ndb.in_transaction() |
| 41 | 64 |
| 42 root_key = cls._GetRootKey() | 65 root_key = cls._GetRootKey(root_id) |
| 43 root = root_key.get() | 66 root = root_key.get() |
| 67 | |
| 44 if not root or not root.current: | 68 if not root or not root.current: |
| 45 return None | 69 return None |
| 46 | 70 |
| 47 if version is None: | 71 if version is None: |
| 48 version = root.current | 72 version = root.current |
| 49 elif version < 1: | 73 elif version < 1: |
| 50 # Return None for versions < 1, which causes exceptions in ndb.Key() | 74 # Return None for versions < 1, which causes exceptions in ndb.Key() |
| 51 return None | 75 return None |
| 52 | 76 |
| 53 return ndb.Key(cls, version, parent=root_key).get() | 77 return ndb.Key(cls, version, parent=root_key).get() |
| 54 | 78 |
| 55 @classmethod | 79 @classmethod |
| 56 def GetLatestVersionNumber(cls): | 80 def GetLatestVersionNumber(cls, root_id=None): |
| 57 root_entity = cls._GetRootKey().get() | 81 root_entity = cls._GetRootKey(root_id).get() |
| 58 if not root_entity: | 82 if not root_entity: |
| 59 return -1 | 83 return -1 |
| 60 return root_entity.current | 84 return root_entity.current |
| 61 | 85 |
| 62 def Save(self): | 86 def Save(self, retry_on_conflict=True): |
| 63 """Saves the current entity, but as a new version.""" | 87 """Saves the current entity, but as a new version. |
| 64 root_key = self._GetRootKey() | 88 |
| 89 Args: | |
| 90 retry_on_conflict (bool): Whether or not the next version number should | |
| 91 automatically be tried in case another transaction writes the entity | |
| 92 first with the same proposed new version number. | |
| 93 | |
| 94 Returns: | |
| 95 The key of the newly written version, and a boolean whether or not this | |
| 96 call to Save() was responsible for creating it. | |
| 97 """ | |
| 98 root_key = self._GetRootKey(self._root_id) | |
| 65 root = root_key.get() or self._GetRootModel()(key=root_key) | 99 root = root_key.get() or self._GetRootModel()(key=root_key) |
| 66 | 100 |
| 67 def SaveData(): | 101 def SaveData(): |
| 68 if self.key.get(): | 102 if self.key.get(): |
| 69 return False # The entity exists, should retry. | 103 return False # The entity exists, should retry. |
| 104 | |
| 70 ndb.put_multi([self, root]) | 105 ndb.put_multi([self, root]) |
| 71 return True | 106 return True |
| 72 | 107 |
| 73 def SetNewKey(): | 108 def SetNewKey(): |
| 74 root.current += 1 | 109 root.current += 1 |
| 75 self.key = ndb.Key(self.__class__, root.current, parent=root_key) | 110 self.key = ndb.Key(self.__class__, root.current, parent=root_key) |
| 76 | 111 |
| 77 SetNewKey() | 112 SetNewKey() |
| 78 while True: | 113 while True: |
| 79 while self.key.get(): | 114 while self.key.get(): |
| 80 SetNewKey() | 115 if retry_on_conflict: |
| 116 SetNewKey() | |
| 117 else: | |
| 118 # Another transaction had already written the proposed new version, so | |
| 119 # return that version's key and False indicating this call to Save() | |
| 120 # was not responsible for creating it. | |
| 121 return self.key, False | |
| 81 | 122 |
| 82 try: | 123 try: |
| 83 if ndb.transaction(SaveData, retries=0): | 124 if ndb.transaction(SaveData, retries=0): |
| 84 return self.key | 125 return self.key, True |
| 85 except ( | 126 except ( |
| 86 datastore_errors.InternalError, | 127 datastore_errors.InternalError, |
| 87 datastore_errors.Timeout, | 128 datastore_errors.Timeout, |
| 88 datastore_errors.TransactionFailedError) as e: | 129 datastore_errors.TransactionFailedError) as e: |
| 89 # https://cloud.google.com/appengine/docs/python/datastore/transactions | 130 # https://cloud.google.com/appengine/docs/python/datastore/transactions |
| 90 # states the result is ambiguous, it could have succeeded. | 131 # states the result is ambiguous, it could have succeeded. |
| 91 logging.info('Transaction likely failed: %s', e) | 132 logging.info('Transaction likely failed: %s', e) |
| 92 except ( | 133 except ( |
| 93 apiproxy_errors.CancelledError, | 134 apiproxy_errors.CancelledError, |
| 94 datastore_errors.BadRequestError, | 135 datastore_errors.BadRequestError, |
| 95 RuntimeError) as e: | 136 RuntimeError) as e: |
| 96 logging.info('Transaction failure: %s', e) | 137 logging.info('Transaction failure: %s', e) |
| 97 else: | 138 else: |
| 98 SetNewKey() | 139 if retry_on_conflict: |
| 140 SetNewKey() | |
| 141 else: | |
| 142 # Another transaction had already written the proposed new version, so | |
| 143 # return that version's key and False indicating this call to Save() | |
| 144 # was not responsible for creating it. | |
| 145 return self.key, False | |
| 99 | 146 |
| 100 @classmethod | 147 @classmethod |
| 101 def _GetRootModel(cls): | 148 def _GetRootModel(cls): |
| 102 """Returns a root model that can be used for versioned entities.""" | 149 """Returns a root model that can be used for versioned entities.""" |
| 103 root_model_name = '%sRoot' % cls.__name__ | 150 root_model_name = '%sRoot' % cls.__name__ |
| 104 | 151 |
| 105 class _RootModel(_GroupRoot): | 152 class _RootModel(_GroupRoot): |
| 106 | 153 |
| 107 @classmethod | 154 @classmethod |
| 108 def _get_kind(cls): | 155 def _get_kind(cls): |
| 109 return root_model_name | 156 return root_model_name |
| 110 | 157 |
| 111 return _RootModel | 158 return _RootModel |
| 112 | 159 |
| 113 @classmethod | 160 @classmethod |
| 114 def _GetRootKey(cls): | 161 def _GetRootKey(cls, root_id=None): |
| 115 return ndb.Key(cls._GetRootModel(), 1) | 162 return ndb.Key( |
| 163 cls._GetRootModel(), root_id if root_id is not None else 1) | |
| OLD | NEW |