#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Implementations of sites and helpers for working with sites.
"""
# NOTE: unicode_literals is NOT imported!!
from __future__ import print_function, absolute_import, division
__docformat__ = "restructuredtext en"
logger = __import__('logging').getLogger(__name__)
from BTrees import family64
from zope import component
from zope import interface
from zope.component.hooks import getSite
from zope.site.site import LocalSiteManager
from zope.site.site import _LocalAdapterRegistry
from zope.interface.interfaces import IComponents
from persistent import Persistent
from nti.schema.fieldproperty import createDirectFieldProperties
from nti.schema.schema import SchemaConfigured
from nti.site.interfaces import ISiteMapping
from nti.site.interfaces import SiteNotFoundError
from nti.site.transient import TrivialSite
from nti.site.transient import HostSiteManager
from zope.component.persistentregistry import PersistentComponents
[docs]def get_alternate_site_name(site_name):
"""
Check for a configured ISiteMapping
"""
site_mapping = component.queryUtility(ISiteMapping, name=site_name)
if site_mapping is not None:
return site_mapping.target_site_name
return None
[docs]def find_site_components(site_names, check_alternate=False):
"""
Return an (global, registered) :class:`.IComponents` implementation named
for the first virtual site found in the sequence of *site_names*.
If no such components can be found, returns none.
"""
for site_name in site_names:
if not site_name: # Empty/default. We want the global. This should only ever be at the end
return None
if check_alternate:
site_name = get_alternate_site_name(site_name)
if site_name is None:
continue
components = component.queryUtility(IComponents, name=site_name)
if components is not None:
return components
return None
_find_site_components = find_site_components # BWC
[docs]def get_site_for_site_names(site_names, site=None):
"""
Return an :class:`.ISite` implementation named for the first virtual site
found in the sequence of site_names.
First, we'll attempt to find the registered persistent site; either given
by the site name or redirected by a registered :class:`ISiteMapping`
pointing to a persistent site. Otherwise, we'll look for a site without the
:class:`ISiteMapping` lookup.
We'll then look a registered persistent site having the same name as the
registered global components found for *site_names*, then that site will be
used. Otherwise, if there is only a registered global components, a
non-persistent site that incorporates those components in the lookup order
while still incorporating the current (or provided) site will be returned.
If no such site or components can be found, returns the fallback
site (the current or provided *site*).
:param site_names: Sequence of strings giving the virtual host names
to use.
:keyword site: If given, this will be the fallback site (and site manager). If
not given, then the currently installed site will be used.
.. versionchanged:: 1.2.0
Look for a :class:`ISiteMapping` registration to map a
non-persistent site to a persistent site.
.. versionchanged:: 1.3.0
Prioritize :class:`ISiteMapping` so that persistent sites can be mapped
to other persistent sites.
"""
if site is None:
site = getSite()
# assert site.getSiteManager().__bases__ == (component.getGlobalSiteManager(),)
# Can we find a named site to use?
site_components = None
if site_names:
# First look for an ISiteMapping
site_components = find_site_components(site_names, check_alternate=True)
if not site_components:
site_components = find_site_components(site_names)
if site_components:
# Yes we can.
site_name = site_components.__name__
# Do we have a persistent site installed in the database? If yes,
# we want to use that.
try:
pers_site = site[u'++etc++hostsites'][site_name]
site = pers_site
except (KeyError, TypeError):
# No, nothing persistent, dummy one up.
# Note that this code path is deprecated now and not
# expected to be hit.
# The site components are only a
# partial configuration and are not persistent, so we need
# to use two bases to make it work (order matters) (for
# example, the main site is almost always the
# 'nti.dataserver' site, where the persistent intid
# utilities live; the named sites do not have those and
# cannot have the persistent nti.dataserver as their real
# base, so the two must be mixed). They are also not
# traversable.
# Host comps used to be simple, but now they may be hierarchacl
# assert site_components.__bases__ == (component.getGlobalSiteManager(),)
# gsm = site_components.__bases__[0]
# assert site_components.adapters.__bases__ == (gsm.adapters,)
# But the current site, when given, must always be the main
# dataserver site
assert isinstance(site, Persistent)
assert isinstance(site.getSiteManager(), Persistent)
main_site = site
# XXX: This easily produces resolution orders that are
# inconsistent with C3. See test_site.test_no_persistent_site.
site_manager = HostSiteManager(main_site.__parent__,
main_site.__name__,
site_components,
main_site.getSiteManager())
site = TrivialSite(site_manager)
site.__parent__ = main_site
site.__name__ = site_name
return site
[docs]def get_component_hierarchy(site=None):
site = getSite() if site is None else site
# XXX: This is tightly coupled. Note that we assume that the parent
# site is a container for the persistent sites.
# There should never be a good reason to need to know this.
hostsites = site.__parent__
site_names = (site.__name__,)
# XXX: Why is this not the same thing as site.getSiteManager()?
components = find_site_components(site_names)
while components is not None:
try:
name = components.__name__
if name in hostsites:
yield components
components = components.__parent__
else:
break
except AttributeError: # pragma: no cover
break
[docs]def get_component_hierarchy_names(site=None, reverse=False):
# XXX This is tightly coupled and there should almost never
# be a good reason to know this.
result = [x.__name__ for x in get_component_hierarchy(site)]
if reverse:
result.reverse()
return result
[docs]class WrongRegistrationTypeError(TypeError):
"""
Raised if an adapter registration is of the wrong type.
.. versionchanged:: 1.0.1
This is no longer raised by this package.
"""
# This used to catch type errors on get() for default comparison,
# but as of BTrees 4.3.2 that no longer happens. The name needs to
# stay around for BWC with existing pickles.
_PermissiveOOBTree = family64.OO.BTree
[docs]class BTreeLocalAdapterRegistry(_LocalAdapterRegistry):
"""
A persistent adapter registry that can switch its internal
data structures to be more persistent friendly when they get large.
.. caution::
This registry doesn't support registrations on bare
classes. This is because the Implements and Provides objects
returned on bare classes do not support comparison or equality
and hence cannot be used in BTrees. (They only hash and compare
equal to *themselves*; within the same process this works out
because of aggressive caching on class objects.) Registering a utility
to provide a bare class is quite hard to do, in any case. Registering
adapters to require bare classes is easier but generally not a best practice.
.. versionchanged:: 3.0.0
No longer converts any data structures as part of mutating this object.
Instead, uses the support from zope.interface 5.3 and zope.component 5.0
to specify the data types to use as they are created on demand.
Existing persistent registries *must* have the ``rebuild()`` method called
on them as part of a migration. The best way to do that would be through
the ``rebuild()`` method on their containing :class:`BTreeLocalSiteManager`.
"""
# Inherit from _LocalAdapterRegistry for maximum compatibility...we are
# going to swizzle out classes. Also, it makes sure we are ILocation.
# Interestingly, we are totally fine to switch out the type from dict
# to BTree. Much of the actual lookup code is implemented in C, but it calls
# into Python for _uncached_lookup, which stays in pure python.
#: The family for the provided map. Defaults to 64-bit maps. I.e., long.
btree_family = family64
# Override types from PersistentAdapterRegistry
_providedType = btree_family.OI.BTree
_mappingType = btree_family.OO.BTree
def _addValueToLeaf(self, existing_leaf_sequence, new_item):
if isinstance(existing_leaf_sequence, tuple):
# We're mutating unmigrated data. This could lead to data loss
# if we have a situation from previous versions of this class like
# BTree -> dict -> tuple; that's about to become
# BTree -> dict -> PersistentList.
# Mutations of either the dict or PersistentList won't notify the
# BTree that it needs to persist itself.
# In the past, the solution to this was to set the BTree conversion threshold
# to 0 so that the intermediate dict got converted to a BTree, but that's
# not possible anymore. So just don't allow it.
raise TypeError("Forbidding mutation of unmigrated data in %r. Call rebuild()."
% self)
return super(BTreeLocalAdapterRegistry, self)._addValueToLeaf(existing_leaf_sequence,
new_item)
[docs]class BTreePersistentComponents(PersistentComponents):
"""
Persistent components that will be friendly to ZODB when they get large.
Note that despite the name, this class is not Persistent, only its
internal components are.
.. caution:: This registry doesn't support bare class registrations.
See :class:`BTreeLocalAdapterRegistry` for details.
"""
btree_family = family64
#: The size at which we will switch from maps to BTrees for registered adapters
#: and registered utilities (individually). This defaults to the maximum size
#: of a BTree bucket before it splits. Thus, when we do this, we will wind up with at
#: least two persistent objects.
btree_threshold = 30
def _init_registries(self):
# NOTE: We cannot simply replace these two attributes at runtime
# or even in a migration (for example, to upgrade from one type to another type)
# and expect it to work. If we are the base of some other Components
# or SiteManager, then these attributes have been copied into the __bases__
# of *its* adapters and utilities. If we swap out our ivar, then the bases
# will be out of sync and lookup will be broken. (BTreeLocalSiteManager
# supposedly keeps track of its subs and so it *could* swap out all of them too.)
self.adapters = BTreeLocalAdapterRegistry()
self.utilities = BTreeLocalAdapterRegistry()
self.adapters.__parent__ = self.utilities.__parent__ = self
self.adapters.__name__ = u'adapters'
self.utilities.__name__ = u'utilities'
def _check_and_btree_map(self, mapping_name):
# The registrations are mappings that look like this:
#
# {(iface, name): (utility, '', None)}
btree_type = self.btree_family.OO.BTree
mapping = getattr(self, mapping_name)
if not isinstance(mapping, btree_type) and len(mapping) > self.btree_threshold:
mapping = btree_type(mapping)
setattr(self, mapping_name, mapping)
# NOTE: This class is *NOT* Persistent, but its subclass BTreeLocalSiteManager
# *is*. That's why __setstate__ is there and not here...it doesn't make much sense here.
[docs] def registerUtility(self, *args, **kwargs): # pylint:disable=arguments-differ
result = super(BTreePersistentComponents, self).registerUtility(*args, **kwargs)
self._check_and_btree_map('_utility_registrations')
return result
[docs] def registerAdapter(self, *args, **kwargs): # pylint:disable=arguments-differ
result = super(BTreePersistentComponents, self).registerAdapter(*args, **kwargs)
self._check_and_btree_map('_adapter_registrations')
return result
[docs]class BTreeLocalSiteManager(BTreePersistentComponents, LocalSiteManager):
"""
Persistent local site manager that will be friendly to ZODB when they
get large.
.. caution:: This registry doesn't support bare class registrations.
See :class:`BTreeLocalAdapterRegistry` for details.
.. versionchanged:: 3.0.0
No longer attempts to change the class of the ``adapters`` and ``utilities``
objects when reading old pickles.
Instead, you must call this object's ``rebuild()`` method as part of a migration.
This method will call ``rebuild()`` on the ``adapters`` and ``utilities`` objects,
and also reset the ``__bases__`` of this object (to its current bases). The
order (leaves first or roots first) shouldn't matter, as long as all registries
in an inheritance hierarchy are committed in a single transaction.
If we detect old versions of the class that haven't been migrated,
we log an error.
"""
# pylint:disable=too-many-ancestors
def __setstate__(self, state):
super(BTreeLocalSiteManager, self).__setstate__(state)
for reg in self.adapters, self.utilities:
if (not isinstance(reg, BTreeLocalAdapterRegistry)
and isinstance(reg, _LocalAdapterRegistry)):
logger.error(
"The LocalSiteManager %r has a sub-object %r that is not yet migrated.",
self, reg
)
[docs] def rebuild(self):
for reg in self.adapters, self.utilities:
if (not isinstance(reg, BTreeLocalAdapterRegistry)
and isinstance(reg, _LocalAdapterRegistry)):
reg.__class__ = BTreeLocalAdapterRegistry
reg.rebuild()
# Setting our bases will cause new references to our *base's*
# .adapters and .utilities to be saved in the ZODB. As long as they migrate
# at the same time, they will get written with their new '__class__', even
# if they are migrated after us.
self.__bases__ = self.__bases__
[docs]@interface.implementer(ISiteMapping)
class SiteMapping(SchemaConfigured):
"""
Maps one site to another.
:raises a :class:`SiteNotFoundError` object if no site found
"""
target_site_name = None
createDirectFieldProperties(ISiteMapping)
[docs] def get_target_site(self):
"""
Returns the target site as defined by this mapping.
"""
current_site = getSite()
site_names = (self.target_site_name,)
result = get_site_for_site_names(site_names, site=current_site)
if result is current_site:
# Invalid mapping
raise SiteNotFoundError("No site found for %s" % self.target_site_name)
return result
# Legacy notes:
# Opening the connection registered it with the transaction manager as an ISynchronizer.
# Ultimately this results in newTransaction being called on the connection object
# at `transaction.begin` time, which in turn syncs the storage. However,
# when multi-databases are used, the other connections DO NOT get this called on them
# if they are implicitly loaded during the course of object traversal or even explicitly
# loaded by name turing an active transaction. This can lead to extra read conflict errors
# (particularly with RelStorage which explicitly polls for invalidations at sync time).
# (Once a multi-db connection has been used, then the next time it would be sync'd. A multi-db
# connection is associated with the same connection to another database for its lifetime, and
# when open()'d will sync all other such connections. Corrollary: ALWAYS go through
# a connection object to get access to multi databases; never go through the database object itself.)
# As a workaround, we iterate across all the databases and sync them manually; this increases the
# cost of handling transactions for things that do not use the other connections, but ensures
# we stay nicely in sync.
# JAM: 2012-09-03: With the database resharding, evaluating the need for this.
# Disabling it.
# for db_name, db in conn.db().databases.items():
# __traceback_info__ = i, db_name, db, func
# if db is None: # For compatibility with databases we no longer use
# continue
# c2 = conn.get_connection(db_name)
# if c2 is conn:
# continue
# c2.newTransaction()
# Now fire 'newTransaction' to the ISynchronizers, including the root connection
# This may result in some redundant fires to sub-connections.