Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(551)

Side by Side Diff: appengine/findit/model/versioned_model.py

Issue 2345093002: [Findit] Extending versioned_model.py to support versioning multiple entities of the same class. (Closed)
Patch Set: Ignore this patch, uploaded unrelated change to wrong branch Created 4 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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 Subclasses will automatically be versioned. To create the first instance of a
30 read and use Save() to write. 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.
31 """ 33 """
32 34
33 @property 35 @property
36 def _root_id(self):
37 return self.key.pairs()[0][1] if self.key else None
38
39 @property
34 def version(self): 40 def version(self):
35 return self.key.integer_id() if self.key else 0 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
36 43
37 @classmethod 44 @classmethod
38 def GetVersion(cls, version=None): 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):
39 """Returns a version of the entity, the latest if version=None.""" 64 """Returns a version of the entity, the latest if version=None."""
40 assert not ndb.in_transaction() 65 assert not ndb.in_transaction()
41 66
42 root_key = cls._GetRootKey() 67 root_key = cls._GetRootKey(key)
43 root = root_key.get() 68 root = root_key.get()
69
44 if not root or not root.current: 70 if not root or not root.current:
45 return None 71 return None
46 72
47 if version is None: 73 if version is None:
48 version = root.current 74 version = root.current
49 elif version < 1: 75 elif version < 1:
50 # Return None for versions < 1, which causes exceptions in ndb.Key() 76 # Return None for versions < 1, which causes exceptions in ndb.Key()
51 return None 77 return None
52 78
53 return ndb.Key(cls, version, parent=root_key).get() 79 return ndb.Key(cls, version, parent=root_key).get()
54 80
55 @classmethod 81 @classmethod
56 def GetLatestVersionNumber(cls): 82 def GetLatestVersionNumber(cls, key=None):
57 root_entity = cls._GetRootKey().get() 83 root_entity = cls._GetRootKey(key).get()
58 if not root_entity: 84 if not root_entity:
59 return -1 85 return -1
60 return root_entity.current 86 return root_entity.current
61 87
62 def Save(self): 88 def Save(self, retry_on_conflict=True):
63 """Saves the current entity, but as a new version.""" 89 """Saves the current entity, but as a new version.
64 root_key = self._GetRootKey() 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)
65 root = root_key.get() or self._GetRootModel()(key=root_key) 101 root = root_key.get() or self._GetRootModel()(key=root_key)
66 102
67 def SaveData(): 103 def SaveData():
68 if self.key.get(): 104 if self.key.get():
69 return False # The entity exists, should retry. 105 return False # The entity exists, should retry.
106
70 ndb.put_multi([self, root]) 107 ndb.put_multi([self, root])
71 return True 108 return True
72 109
73 def SetNewKey(): 110 def SetNewKey():
74 root.current += 1 111 root.current += 1
75 self.key = ndb.Key(self.__class__, root.current, parent=root_key) 112 self.key = ndb.Key(self.__class__, root.current, parent=root_key)
76 113
77 SetNewKey() 114 SetNewKey()
78 while True: 115 while True:
79 while self.key.get(): 116 while self.key.get():
80 SetNewKey() 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
81 124
82 try: 125 try:
83 if ndb.transaction(SaveData, retries=0): 126 if ndb.transaction(SaveData, retries=0):
84 return self.key 127 return self.key, True
85 except ( 128 except (
86 datastore_errors.InternalError, 129 datastore_errors.InternalError,
87 datastore_errors.Timeout, 130 datastore_errors.Timeout,
88 datastore_errors.TransactionFailedError) as e: 131 datastore_errors.TransactionFailedError) as e:
89 # https://cloud.google.com/appengine/docs/python/datastore/transactions 132 # https://cloud.google.com/appengine/docs/python/datastore/transactions
90 # states the result is ambiguous, it could have succeeded. 133 # states the result is ambiguous, it could have succeeded.
91 logging.info('Transaction likely failed: %s', e) 134 logging.info('Transaction likely failed: %s', e)
92 except ( 135 except (
93 apiproxy_errors.CancelledError, 136 apiproxy_errors.CancelledError,
94 datastore_errors.BadRequestError, 137 datastore_errors.BadRequestError,
95 RuntimeError) as e: 138 RuntimeError) as e:
96 logging.info('Transaction failure: %s', e) 139 logging.info('Transaction failure: %s', e)
97 else: 140 else:
98 SetNewKey() 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
99 148
100 @classmethod 149 @classmethod
101 def _GetRootModel(cls): 150 def _GetRootModel(cls):
102 """Returns a root model that can be used for versioned entities.""" 151 """Returns a root model that can be used for versioned entities."""
103 root_model_name = '%sRoot' % cls.__name__ 152 root_model_name = '%sRoot' % cls.__name__
104 153
105 class _RootModel(_GroupRoot): 154 class _RootModel(_GroupRoot):
106 155
107 @classmethod 156 @classmethod
108 def _get_kind(cls): 157 def _get_kind(cls):
109 return root_model_name 158 return root_model_name
110 159
111 return _RootModel 160 return _RootModel
112 161
113 @classmethod 162 @classmethod
114 def _GetRootKey(cls): 163 def _GetRootKey(cls, key=None):
115 return ndb.Key(cls._GetRootModel(), 1) 164 return ndb.Key(cls._GetRootModel(), key if key is not None else 1)
OLDNEW
« no previous file with comments | « appengine/findit/model/versioned_config.py ('k') | appengine/findit/templates/flake/result.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698