blob: 1c76a71cea7ba132387f9d61bd2fcbed61b6a2d6 [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Provides a model to support versioned entities in datastore.
Idea: use a root model entity to keep track of the most recent version of a
versioned entity, and make the versioned entities and the root model entity in
the same entity group so that they could be read and written in a transaction.
"""
import logging
from google.appengine.api import datastore_errors
from google.appengine.ext import ndb
from google.appengine.runtime import apiproxy_errors
class _GroupRoot(ndb.Model):
"""Root entity of a group to support versioned children."""
# Key id of the most recent child entity in the datastore. It is monotonically
# increasing and is 0 if no child is present.
current = ndb.IntegerProperty(indexed=False, default=0)
class VersionedModel(ndb.Model):
"""A model that supports versioning.
Subclasses will automatically be versioned. To create the first instance of a
versioned entity, use Create(key) with optional key to differentiate between
multiple unique entities of the same subclass. Use GetVersion() to read and
Save() to write.
"""
@property
def _root_id(self):
return self.key.pairs()[0][1] if self.key else None
@property
def version_number(self):
# Ndb treats key.integer_id() of 0 as None, so default to 0.
return self.key.integer_id() or 0 if self.key else 0
@classmethod
def Create(cls, key=None):
"""Creates an instance of cls that is to become the first version.
The calling function of Create() should be responsible first for checking
no previous version of the proposed entity already exists.
Args:
key: Any user-specified value that will serve as the id for the root
entity's key.
Returns:
An instance of cls meant to be the first version. Note for this instance
to be committed to the datastore Save() would need to be called on the
instance returned by this method.
"""
return cls(key=ndb.Key(cls, 0, parent=cls._GetRootKey(key)))
@classmethod
def GetVersion(cls, key=None, version=None):
"""Returns a version of the entity, the latest if version=None."""
assert not ndb.in_transaction()
root_key = cls._GetRootKey(key)
root = root_key.get()
if not root or not root.current:
return None
if version is None:
version = root.current
elif version < 1:
# Return None for versions < 1, which causes exceptions in ndb.Key()
return None
return ndb.Key(cls, version, parent=root_key).get()
@classmethod
def GetLatestVersionNumber(cls, key=None):
root_entity = cls._GetRootKey(key).get()
if not root_entity:
return -1
return root_entity.current
def Save(self, retry_on_conflict=True):
"""Saves the current entity, but as a new version.
Args:
retry_on_conflict (bool): Whether or not the next version number should
automatically be tried in case another transaction writes the entity
first with the same proposed new version number.
Returns:
The key of the newly written version, and a boolean whether or not this
call to Save() was responsible for creating it.
"""
root_key = self._GetRootKey(self._root_id)
root = root_key.get() or self._GetRootModel()(key=root_key)
def SaveData():
if self.key.get():
return False # The entity exists, should retry.
ndb.put_multi([self, root])
return True
def SetNewKey():
root.current += 1
self.key = ndb.Key(self.__class__, root.current, parent=root_key)
SetNewKey()
while True:
while self.key.get():
if retry_on_conflict:
SetNewKey()
else:
# Another transaction had already written the proposed new version, so
# return that version's key and False indicating this call to Save()
# was not responsible for creating it.
return self.key, False
try:
if ndb.transaction(SaveData, retries=0):
return self.key, True
except (datastore_errors.InternalError, datastore_errors.Timeout,
datastore_errors.TransactionFailedError) as e:
# https://cloud.google.com/appengine/docs/python/datastore/transactions
# states the result is ambiguous, it could have succeeded.
logging.info('Transaction likely failed: %s', e)
except (apiproxy_errors.CancelledError, datastore_errors.BadRequestError,
RuntimeError) as e:
logging.info('Transaction failure: %s', e)
else:
if retry_on_conflict:
SetNewKey()
else:
# Another transaction had already written the proposed new version, so
# return that version's key and False indicating this call to Save()
# was not responsible for creating it.
return self.key, False
@classmethod
def _GetRootModel(cls):
"""Returns a root model that can be used for versioned entities."""
root_model_name = '%sRoot' % cls.__name__
class _RootModel(_GroupRoot):
@classmethod
def _get_kind(cls):
return root_model_name
return _RootModel
@classmethod
def _GetRootKey(cls, key=None):
return ndb.Key(cls._GetRootModel(), key if key is not None else 1)