if not entity.key or not entity.key.id(): break try: if (yield txn.transaction_async(run, retries=0)): break except txn.CommitError: # Retry with the same key. pass else: # Entity existed. Get the next key. entity.key = yield new_key_callback_async() raise ndb.Return(entity.key) insert = utils.sync_of(insert_async) def get_versioned_root_model(model_name): """Returns a root model that can be used for versioned entities. Using this entity for get_versioned_most_recent(), get_versioned_most_recent_with_root() and store_new_version() is optional. Any entity with cls.current as an ndb.IntegerProperty will do. """ assert isinstance(model_name, str), model_name class _Root(Root): @classmethod def _get_kind(cls): return model_name
class GlobalConfig(ndb.Model): """Singleton entity with the global configuration of the service. All changes are stored in the revision log. """ # When this revision of configuration was created. updated_ts = ndb.DateTimeProperty(indexed=False, auto_now_add=True) # Who created this revision of configuration (as identity string). updated_by = ndb.StringProperty(indexed=False, default='') @classmethod def cached_async(cls): """Fetches config entry from local cache or datastore. Bootstraps it if missing. May return slightly stale data but in most cases doesn't do any RPCs. Should be used for read-only access to config. """ # Build new class-specific fetcher function with cache on the fly on # the first attempt (it's not a big deal if it happens concurrently in MT # environment, last one wins). Same can be achieved with metaclasses, but no # one likes metaclasses. if not cls._config_fetcher_async: @ndb.tasklet def fetcher(): with fetcher.cache_lock: expiry = fetcher.cache_expiry if expiry is not None and utils.utcnow() < expiry: raise ndb.Return(fetcher.cache_value) # Do not lock while yielding, it would cause deadlock. # Also do not cache a future, it might cross ndb context boundary. # If there is no cached value, multiple concurrent requests will make # multiple RPCs, but as soon as one of them updates cache, subsequent # requests will use the cached value, for a minute. conf = yield cls.fetch_async() if not conf: conf = cls() conf.set_defaults() yield conf.store_async(updated_by='') with fetcher.cache_lock: fetcher.cache_expiry = utils.utcnow() + datetime.timedelta( minutes=1) fetcher.cache_value = conf raise ndb.Return(conf) fetcher.cache_lock = threading.Lock() fetcher.cache_expiry = None fetcher.cache_value = None cls._config_fetcher_async = staticmethod(fetcher) return cls._config_fetcher_async() cached = utils.sync_of(cached_async) @classmethod def clear_cache(cls): """Clears the cache of .cached(). So the next call to .cached() returns the fresh instance from ndb. """ if cls._config_fetcher_async: cls._config_fetcher_async.cache_expiry = None @classmethod def fetch_async(cls): """Returns the current up-to-date version of the config entity. Always fetches it from datastore. May return None if missing. """ return datastore_utils.get_versioned_most_recent_async( cls, cls._get_root_key()) fetch = utils.sync_of(fetch_async) def store_async(self, updated_by): """Stores a new version of the config entity.""" # Create an incomplete key, to be completed by 'store_new_version'. self.key = ndb.Key(self.__class__, None, parent=self._get_root_key()) self.updated_by = updated_by self.updated_ts = utils.utcnow() return datastore_utils.store_new_version_async(self, self._get_root_model()) store = utils.sync_of(store_async) def modify(self, updated_by, **kwargs): """Applies |kwargs| dict to the entity and stores the entity if changed.""" dirty = False for k, v in kwargs.items(): assert k in self._properties, k if getattr(self, k) != v: setattr(self, k, v) dirty = True if dirty: self.store(updated_by) return dirty def set_defaults(self): """Fills in default values for empty config. Implemented by subclasses.""" ### Private stuff. _config_fetcher_async = None @classmethod def _get_root_model(cls): return datastore_utils.get_versioned_root_model('%sRoot' % cls.__name__) @classmethod def _get_root_key(cls): return ndb.Key(cls._get_root_model(), 1)