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 Subclasses will automatically be versioned. To create the first instance of a | |
30 versioned entity, use Create(key) with optional key to differentiate between | |
31 multiple unique entities of the same subclass. Use GetVersion() to read and | |
32 Save() to write. | |
33 """ | |
34 | |
35 @property | |
36 def _root_id(self): | |
37 return self.key.pairs()[0][1] if self.key else None | |
38 | |
39 @property | |
40 def version_number(self): | |
41 # Ndb treats key.integer_id() of 0 as None, so default to 0. | |
42 return self.key.integer_id() or 0 if self.key else 0 | |
43 | |
44 @classmethod | |
45 def Create(cls, key=None): | |
46 """Creates an instance of cls that is to become the first version. | |
47 | |
48 The calling function of Create() should be responsible first for checking | |
49 no previous version of the proposed entity already exists. | |
50 | |
51 Args: | |
52 key: Any user-specified value that will serve as the id for the root | |
53 entity's key. | |
54 | |
55 Returns: | |
56 An instance of cls meant to be the first version. Note for this instance | |
57 to be committed to the datastore Save() would need to be called on the | |
58 instance returned by this method. | |
59 """ | |
60 return cls(key=ndb.Key(cls, 0, parent=cls._GetRootKey(key))) | |
61 | |
62 @classmethod | |
63 def GetVersion(cls, key=None, version=None): | |
64 """Returns a version of the entity, the latest if version=None.""" | |
65 assert not ndb.in_transaction() | |
66 | |
67 root_key = cls._GetRootKey(key) | |
68 root = root_key.get() | |
69 | |
70 if not root or not root.current: | |
71 return None | |
72 | |
73 if version is None: | |
74 version = root.current | |
75 elif version < 1: | |
76 # Return None for versions < 1, which causes exceptions in ndb.Key() | |
77 return None | |
78 | |
79 return ndb.Key(cls, version, parent=root_key).get() | |
80 | |
81 @classmethod | |
82 def GetLatestVersionNumber(cls, key=None): | |
83 root_entity = cls._GetRootKey(key).get() | |
84 if not root_entity: | |
85 return -1 | |
86 return root_entity.current | |
87 | |
88 def Save(self, retry_on_conflict=True): | |
89 """Saves the current entity, but as a new version. | |
90 | |
91 Args: | |
92 retry_on_conflict (bool): Whether or not the next version number should | |
93 automatically be tried in case another transaction writes the entity | |
94 first with the same proposed new version number. | |
95 | |
96 Returns: | |
97 The key of the newly written version, and a boolean whether or not this | |
98 call to Save() was responsible for creating it. | |
99 """ | |
100 root_key = self._GetRootKey(self._root_id) | |
101 root = root_key.get() or self._GetRootModel()(key=root_key) | |
102 | |
103 def SaveData(): | |
104 if self.key.get(): | |
105 return False # The entity exists, should retry. | |
106 | |
107 ndb.put_multi([self, root]) | |
108 return True | |
109 | |
110 def SetNewKey(): | |
111 root.current += 1 | |
112 self.key = ndb.Key(self.__class__, root.current, parent=root_key) | |
113 | |
114 SetNewKey() | |
115 while True: | |
116 while self.key.get(): | |
117 if retry_on_conflict: | |
118 SetNewKey() | |
119 else: | |
120 # Another transaction had already written the proposed new version, so | |
121 # return that version's key and False indicating this call to Save() | |
122 # was not responsible for creating it. | |
123 return self.key, False | |
124 | |
125 try: | |
126 if ndb.transaction(SaveData, retries=0): | |
127 return self.key, True | |
128 except ( | |
129 datastore_errors.InternalError, | |
130 datastore_errors.Timeout, | |
131 datastore_errors.TransactionFailedError) as e: | |
132 # https://cloud.google.com/appengine/docs/python/datastore/transactions | |
133 # states the result is ambiguous, it could have succeeded. | |
134 logging.info('Transaction likely failed: %s', e) | |
135 except ( | |
136 apiproxy_errors.CancelledError, | |
137 datastore_errors.BadRequestError, | |
138 RuntimeError) as e: | |
139 logging.info('Transaction failure: %s', e) | |
140 else: | |
141 if retry_on_conflict: | |
142 SetNewKey() | |
143 else: | |
144 # Another transaction had already written the proposed new version, so | |
145 # return that version's key and False indicating this call to Save() | |
146 # was not responsible for creating it. | |
147 return self.key, False | |
148 | |
149 @classmethod | |
150 def _GetRootModel(cls): | |
151 """Returns a root model that can be used for versioned entities.""" | |
152 root_model_name = '%sRoot' % cls.__name__ | |
153 | |
154 class _RootModel(_GroupRoot): | |
155 | |
156 @classmethod | |
157 def _get_kind(cls): | |
158 return root_model_name | |
159 | |
160 return _RootModel | |
161 | |
162 @classmethod | |
163 def _GetRootKey(cls, key=None): | |
164 return ndb.Key(cls._GetRootModel(), key if key is not None else 1) | |
OLD | NEW |