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 """ |
| (...skipping 13 matching lines...) Expand all Loading... | |
| 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 |
| 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_string_id(self): | |
|
stgao
2016/09/22 00:44:01
Why do we care whether the root id is a string or
stgao
2016/09/22 00:44:01
Also not make it a public property.
lijeffrey
2016/09/22 17:36:15
Done.
| |
| 35 if not self.key: | |
| 36 return None | |
| 37 | |
| 38 key_pairs = self.key.pairs() | |
| 39 root_string_id = key_pairs[0][1] | |
| 40 | |
| 41 if len(key_pairs) > 1: | |
| 42 # The key is expected to have format Key(root, string_id, kind, version) | |
| 43 return root_string_id | |
| 44 | |
| 45 if isinstance(root_string_id, basestring): | |
| 46 # The key has format Key(kind, string_id) | |
| 47 return root_string_id | |
| 48 | |
| 49 # Otherwise, the key has format Key(root, version_number). | |
| 50 return None | |
| 51 | |
| 52 @property | |
| 34 def version(self): | 53 def version(self): |
| 35 return self.key.integer_id() if self.key else 0 | 54 if not self.key: |
| 55 return 0 | |
| 56 | |
| 57 key_pairs = self.key.pairs() | |
| 58 if len(key_pairs) == 1: | |
| 59 # Key(root, version_number) or Key(kind, string_id). The latter is used | |
|
stgao
2016/09/22 00:44:01
This is the child entity of the root.
Per discuss
lijeffrey
2016/09/22 17:36:14
A slight change is actually needed:
When creating
stgao
2016/09/23 06:27:14
Should we fix the problem in the subclasses (MFA h
stgao
2016/09/23 16:35:53
Another option is to add a function here for insta
| |
| 60 # only when an entity has been created but not yet written to ndb. | |
| 61 return self.key.integer_id() or 0 | |
| 62 | |
| 63 # Key(root, string_id, type, version_number). | |
| 64 return key_pairs[-1][-1] | |
| 36 | 65 |
| 37 @classmethod | 66 @classmethod |
| 38 def GetVersion(cls, version=None): | 67 def GetVersion(cls, root_string_id=None, version=None): |
| 39 """Returns a version of the entity, the latest if version=None.""" | 68 """Returns a version of the entity, the latest if version=None.""" |
| 40 assert not ndb.in_transaction() | 69 assert not ndb.in_transaction() |
| 41 | 70 |
| 42 root_key = cls._GetRootKey() | 71 root_key = cls._GetRootKey(root_string_id) |
| 43 root = root_key.get() | 72 root = root_key.get() |
| 73 | |
| 44 if not root or not root.current: | 74 if not root or not root.current: |
| 45 return None | 75 return None |
| 46 | 76 |
| 47 if version is None: | 77 if version is None: |
| 48 version = root.current | 78 version = root.current |
| 49 elif version < 1: | 79 elif version < 1: |
| 50 # Return None for versions < 1, which causes exceptions in ndb.Key() | 80 # Return None for versions < 1, which causes exceptions in ndb.Key() |
| 51 return None | 81 return None |
| 52 | 82 |
| 53 return ndb.Key(cls, version, parent=root_key).get() | 83 return ndb.Key(cls, version, parent=root_key).get() |
| 54 | 84 |
| 55 @classmethod | 85 @classmethod |
| 56 def GetLatestVersionNumber(cls): | 86 def GetLatestVersionNumber(cls, root_string_id=None): |
| 57 root_entity = cls._GetRootKey().get() | 87 root_entity = cls._GetRootKey(root_string_id).get() |
| 58 if not root_entity: | 88 if not root_entity: |
| 59 return -1 | 89 return -1 |
| 60 return root_entity.current | 90 return root_entity.current |
| 61 | 91 |
| 62 def Save(self): | 92 def Save(self, should_try_next_version_number=True): |
|
stgao
2016/09/22 00:44:01
name nit: too long. How about retry_on_conflict or
lijeffrey
2016/09/22 17:36:15
Done.
| |
| 63 """Saves the current entity, but as a new version.""" | 93 """Saves the current entity, but as a new version. |
| 64 root_key = self._GetRootKey() | 94 |
| 95 Args: | |
| 96 should_try_next_version_number: Boolean whether or not the next version | |
|
stgao
2016/09/22 00:44:01
Format nit:
Args:
param (type): comment.
lijeffrey
2016/09/22 17:36:14
Done.
| |
| 97 number should automatically be tried in case another transaction writes | |
| 98 the entity first with the same proposed new version number. | |
| 99 | |
| 100 Returns: | |
| 101 The key of the newly written version, and a boolean whether or not this | |
| 102 call to Save() was responsible for creating it. | |
| 103 """ | |
| 104 root_string_id = self.root_string_id | |
|
stgao
2016/09/22 00:44:01
nit: root_string_id is used only once.
lijeffrey
2016/09/22 17:36:14
Done.
| |
| 105 root_key = self._GetRootKey(root_string_id) | |
| 65 root = root_key.get() or self._GetRootModel()(key=root_key) | 106 root = root_key.get() or self._GetRootModel()(key=root_key) |
| 66 | 107 |
| 67 def SaveData(): | 108 def SaveData(): |
| 68 if self.key.get(): | 109 if self.key.get(): |
| 69 return False # The entity exists, should retry. | 110 return False # The entity exists, should retry. |
| 111 | |
| 70 ndb.put_multi([self, root]) | 112 ndb.put_multi([self, root]) |
| 71 return True | 113 return True |
| 72 | 114 |
| 73 def SetNewKey(): | 115 def SetNewKey(): |
| 74 root.current += 1 | 116 root.current += 1 |
| 75 self.key = ndb.Key(self.__class__, root.current, parent=root_key) | 117 self.key = ndb.Key(self.__class__, root.current, parent=root_key) |
| 76 | 118 |
| 77 SetNewKey() | 119 SetNewKey() |
| 78 while True: | 120 while True: |
| 79 while self.key.get(): | 121 while self.key.get(): |
| 80 SetNewKey() | 122 if should_try_next_version_number: |
| 123 SetNewKey() | |
| 124 else: | |
| 125 # Another transaction had already written the proposed new version, so | |
| 126 # return that version's key and False indicating this call to Save() | |
| 127 # was not responsible for creating it. | |
| 128 return self.key, False | |
| 81 | 129 |
| 82 try: | 130 try: |
| 83 if ndb.transaction(SaveData, retries=0): | 131 if ndb.transaction(SaveData, retries=0): |
| 84 return self.key | 132 return self.key, True |
| 85 except ( | 133 except ( |
| 86 datastore_errors.InternalError, | 134 datastore_errors.InternalError, |
| 87 datastore_errors.Timeout, | 135 datastore_errors.Timeout, |
| 88 datastore_errors.TransactionFailedError) as e: | 136 datastore_errors.TransactionFailedError) as e: |
| 89 # https://cloud.google.com/appengine/docs/python/datastore/transactions | 137 # https://cloud.google.com/appengine/docs/python/datastore/transactions |
| 90 # states the result is ambiguous, it could have succeeded. | 138 # states the result is ambiguous, it could have succeeded. |
| 91 logging.info('Transaction likely failed: %s', e) | 139 logging.info('Transaction likely failed: %s', e) |
| 92 except ( | 140 except ( |
| 93 apiproxy_errors.CancelledError, | 141 apiproxy_errors.CancelledError, |
| 94 datastore_errors.BadRequestError, | 142 datastore_errors.BadRequestError, |
| 95 RuntimeError) as e: | 143 RuntimeError) as e: |
| 96 logging.info('Transaction failure: %s', e) | 144 logging.info('Transaction failure: %s', e) |
| 97 else: | 145 else: |
| 98 SetNewKey() | 146 if should_try_next_version_number: |
| 147 SetNewKey() | |
| 148 else: | |
| 149 # Another transaction had already written the proposed new version, so | |
| 150 # return that version's key and False indicating this call to Save() | |
| 151 # was not responsible for creating it. | |
| 152 return self.key, False | |
|
chanli
2016/09/22 00:11:25
Based on https://docs.python.org/3/tutorial/errors
stgao
2016/09/22 00:44:01
There is a if in line #131. We return only when we
| |
| 99 | 153 |
| 100 @classmethod | 154 @classmethod |
| 101 def _GetRootModel(cls): | 155 def _GetRootModel(cls): |
| 102 """Returns a root model that can be used for versioned entities.""" | 156 """Returns a root model that can be used for versioned entities.""" |
| 103 root_model_name = '%sRoot' % cls.__name__ | 157 root_model_name = '%sRoot' % cls.__name__ |
| 104 | 158 |
| 105 class _RootModel(_GroupRoot): | 159 class _RootModel(_GroupRoot): |
| 106 | 160 |
| 107 @classmethod | 161 @classmethod |
| 108 def _get_kind(cls): | 162 def _get_kind(cls): |
| 109 return root_model_name | 163 return root_model_name |
| 110 | 164 |
| 111 return _RootModel | 165 return _RootModel |
| 112 | 166 |
| 113 @classmethod | 167 @classmethod |
| 114 def _GetRootKey(cls): | 168 def _GetRootKey(cls, string_id=None): |
|
stgao
2016/09/22 00:44:01
name nit: root_id to be consistent.
lijeffrey
2016/09/22 17:36:15
Done.
| |
| 115 return ndb.Key(cls._GetRootModel(), 1) | 169 return ndb.Key( |
| 170 cls._GetRootModel(), string_id if string_id is not None else 1) | |
| OLD | NEW |