Implementación de la arquitectura multicliente mediante espacios de nombre

La API Namespaces te permite habilitar fácilmente la arquitectura multicliente en tu aplicación. Para ello, solo tienes que seleccionar una cadena de espacio de nombres para cada cliente en appengine_config.py mediante el paquete namespace_manager.

Configurar el espacio de nombres actual

Puedes obtener, definir y validar espacios de nombres con namespace_manager. El gestor de espacios de nombres te permite definir un espacio de nombres actual para las APIs con espacios de nombres. Puedes configurar un espacio de nombres actual por adelantado con appengine_config.py. El almacén de datos y la caché de memoria usarán automáticamente ese espacio de nombres.

La mayoría de los desarrolladores de App Engine usarán su dominio de Google Workspace (antes G Suite) como espacio de nombres actual. Google Workspace te permite implementar tu aplicación en cualquier dominio que tengas, por lo que puedes usar este mecanismo fácilmente para configurar diferentes espacios de nombres para diferentes dominios. Después, puedes usar esos espacios de nombres independientes para segregar los datos de los dominios. Para obtener más información, consulta Asignar dominios personalizados.

En el siguiente fragmento de código se muestra cómo definir el espacio de nombres actual en el dominio de Google Workspace que se ha usado para asignar la URL. Es importante destacar que esta cadena será la misma para todas las URLs asignadas a través del mismo dominio de Google Workspace.

Para definir un espacio de nombres en Python, usa el sistema de configuración de App Engine appengine_config.py en el directorio raíz de tu aplicación. En el siguiente ejemplo sencillo se muestra cómo usar tu dominio de Google Workspace como espacio de nombres actual:

from google.appengine.api import namespace_manager

# Called only if the current namespace is not set.
def namespace_manager_default_namespace_for_request():
    # The returned string will be used as the Google Apps domain.
    return namespace_manager.google_apps_namespace()

Si no especifica ningún valor para namespace, el espacio de nombres se define como una cadena vacía. La cadena namespace es arbitraria, pero también está limitada a un máximo de 100 caracteres alfanuméricos, puntos, guiones bajos y guiones. En concreto, las cadenas de espacio de nombres deben coincidir con la expresión regular [0-9A-Za-z._-]{0,100}.

Por convención, todos los espacios de nombres que empiezan por "_" (guion bajo) están reservados para el uso del sistema. Esta regla acerca de los espacios de nombre del sistema no es obligatoria, aunque si la cumples seguramente evitarás consecuencias negativas de distinta índole.

Para obtener información más general sobre la configuración de appengine_config.py, consulta Configuración del módulo Python.

Evitar las filtraciones de datos

Uno de los riesgos comúnmente asociados a las aplicaciones multicliente es el peligro de la pérdida de datos en los espacios de nombre. Las pérdidas de datos accidentales pueden originarse por varios motivos, entre los que se incluye:

  • El uso de espacios de nombre con API de App Engine que aún no son compatibles con los espacios de nombre. Por ejemplo, el almacén de blob no admite el uso de espacios de nombre. Si usas espacios de nombres con Blobstore, debes evitar usar consultas de Blobstore para solicitudes de usuarios finales o claves de Blobstore de fuentes no fiables.
  • Usar un medio de almacenamiento externo (en lugar de memcache y datastore), a través de URL Fetch u otro mecanismo, sin proporcionar un esquema de compartimentación para los espacios de nombres.
  • Definir un espacio de nombres en función del dominio de correo de un usuario. En la mayoría de los casos, no querrá que todas las direcciones de correo de un dominio accedan a un espacio de nombres. El uso del dominio de correo electrónico también evita que tu aplicación utilice un espacio de nombre hasta el momento en que el usuario haya accedido a la cuenta.

Desplegar espacios de nombres

En las secciones siguientes se describe cómo implementar espacios de nombre con otras herramientas y API de App Engine.

Crear espacios de nombres por usuario

Algunas aplicaciones necesitan crear espacios de nombre para cada usuario. Si quieres compartimentar los datos a nivel de usuario de los usuarios que han iniciado sesión, te recomendamos que uses User.user_id(), que devuelve un ID único y permanente para el usuario. En el siguiente código de ejemplo se muestra cómo utilizar el API de usuarios con este fin:

from google.appengine.api import users

def namespace_manager_default_namespace_for_request():
    # assumes the user is logged in.
    return users.get_current_user().user_id()

Normalmente, las aplicaciones que crean espacios de nombres por usuario también proporcionan páginas de destino específicas a diferentes usuarios. En estos casos, la aplicación debe proporcionar un esquema de URL que indique la página de destino que se mostrará a un usuario.

Usar espacios de nombres con Datastore

De forma predeterminada, el almacén de datos utiliza la configuración de espacios de nombre del administrador de espacios de nombre para las solicitudes del almacén de datos. La API aplica este espacio de nombres actual a los objetos Key o Query cuando se crean. Por lo tanto, debes tener cuidado si una aplicación almacena objetos Key o Query en formatos serializados, ya que el espacio de nombres se conserva en esas serializaciones.

Si utilizas objetos Key y Query deserializados, asegúrate de que se comporten como esperas. La mayoría de las aplicaciones sencillas que usan el almacén de datos (put/query/get) sin usar otros mecanismos de almacenamiento funcionarán correctamente si se define el espacio de nombres actual antes de llamar a cualquier API del almacén de datos.

Los objetos Query y Key muestran los siguientes comportamientos únicos en relación con los espacios de nombres:

  • Los objetos Query y Key heredan el espacio de nombres actual cuando se construyen, a menos que definas un espacio de nombres explícito.
  • Cuando una aplicación crea un nuevo Key a partir de un elemento antecesor, el nuevo Key hereda el espacio de nombres del elemento antecesor.

En el siguiente ejemplo de código se muestra un controlador de solicitudes de ejemplo que incrementa un contador en el almacén de datos para el espacio de nombres global y un espacio de nombres especificado arbitrariamente.

from google.appengine.api import namespace_manager
from google.appengine.ext import ndb
import webapp2


class Counter(ndb.Model):
    count = ndb.IntegerProperty()


@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count


class DatastoreCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = update_counter('counter')

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = update_counter('counter')
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))


app = webapp2.WSGIApplication([
    (r'/datastore', DatastoreCounterHandler),
    (r'/datastore/(.*)', DatastoreCounterHandler)
], debug=True)

Usar espacios de nombres con Memcache

De forma predeterminada, el servicio Memcache utiliza el espacio de nombre del administrador de espacios de nombre para las solicitudes de la memoria caché. En la mayoría de los casos, no es necesario que establezcas explícitamente un espacio de nombre en Memcache y, si lo haces, puedes provocar errores inesperados.

No obstante, en algunos casos concretos resulta adecuado establecer explícitamente un espacio de nombre en Memcache. Por ejemplo, es posible que tu aplicación comparta datos comunes entre todos los espacios de nombre (como una tabla que incluya códigos de países).

En el siguiente fragmento de código se muestra cómo establecer explícitamente el espacio de nombre en Memcache:

Mediante el API Python para el servicio Memcache, puedes obtener el espacio de nombre actual desde el administrador de espacios de nombre o establecerlo explícitamente cuando crees el servicio Memcache.

En el siguiente ejemplo de código se muestra un controlador de solicitudes de ejemplo que incrementa un contador en memcache para el espacio de nombres global y un espacio de nombres especificado de forma arbitraria.

from google.appengine.api import memcache
from google.appengine.api import namespace_manager
import webapp2


class MemcacheCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = memcache.incr('counter', initial_value=0)

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = memcache.incr('counter', initial_value=0)
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))


app = webapp2.WSGIApplication([
    (r'/memcache', MemcacheCounterHandler),
    (r'/memcache/(.*)', MemcacheCounterHandler)
], debug=True)

El siguiente ejemplo establece el espacio de nombre explícitamente cuando almacenas el valor en Memcache:

  // Store an entry to the memcache explicitly
memcache.add("key", data, namespace='abc')

Usar espacios de nombres con la cola de tareas

De forma predeterminada, las colas de inserción usan el espacio de nombres actual tal como se haya definido en el gestor de espacios de nombres en el momento en que se creó la tarea. En la mayoría de los casos, no es necesario establecer explícitamente un espacio de nombre en la cola de tareas y, si lo haces, puedes provocar errores inesperados.

Los nombres de tareas se comparten entre todos los espacios de nombre. No puedes crear dos tareas con el mismo nombre, aunque utilicen espacios de nombre distintos. Si quieres utilizar el mismo nombre de tarea para varios espacios de nombre, puedes anexar cada uno de ellos al mismo nombre de tarea.

Cuando una tarea nueva llama al método add() de la cola de tareas, esta copia el espacio de nombres actual y (si procede) el dominio de Google Workspace del gestor de espacios de nombres. Cuando se ejecuta la tarea, se restauran el espacio de nombres actual y el espacio de nombres de Google Workspace.

Si el espacio de nombres actual no se ha definido en la solicitud original (es decir, si get_namespace() devuelve ''), puedes usar set_namespace() para definir el espacio de nombres actual de la tarea.

En algunos casos concretos resulta adecuado establecer explícitamente un espacio de nombre para una tarea que se aplique a todos los espacios de nombre. Por ejemplo, puedes crear una tarea que ofrezca los valores totales de las estadísticas de uso en todos los espacios de nombre. Después, puedes establecer explícitamente el espacio de nombre de la tarea. En el siguiente código de ejemplo se muestra cómo establecer explícitamente espacios de nombre con la cola de tareas.

from google.appengine.api import namespace_manager
from google.appengine.api import taskqueue
from google.appengine.ext import ndb
import webapp2


class Counter(ndb.Model):
    count = ndb.IntegerProperty()


@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count


def get_count(name):
    counter = Counter.get_by_id(name)
    if not counter:
        return 0
    return counter.count


class DeferredCounterHandler(webapp2.RequestHandler):
    def post(self):
        name = self.request.get('counter_name')
        update_counter(name)


class TaskQueueCounterHandler(webapp2.RequestHandler):
    """Queues two tasks to increment a counter in global namespace as well as
    the namespace is specified by the request, which is arbitrarily named
    'default' if not specified."""
    def get(self, namespace='default'):
        # Queue task to update global counter.
        current_global_count = get_count('counter')
        taskqueue.add(
            url='/tasks/counter',
            params={'counter_name': 'counter'})

        # Queue task to update counter in specified namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            current_namespace_count = get_count('counter')
            taskqueue.add(
                url='/tasks/counter',
                params={'counter_name': 'counter'})
        finally:
            namespace_manager.set_namespace(previous_namespace)

        self.response.write(
            'Counters will be updated asyncronously.'
            'Current values: Global: {}, Namespace {}: {}'.format(
                current_global_count, namespace, current_namespace_count))


app = webapp2.WSGIApplication([
    (r'/tasks/counter', DeferredCounterHandler),
    (r'/taskqueue', TaskQueueCounterHandler),
    (r'/taskqueue/(.*)', TaskQueueCounterHandler)
], debug=True)

Usar espacios de nombres con Blobstore

El almacén de blob no está segmentado por espacio de nombre. Para conservar un espacio de nombre en el almacén de blob, es necesario que accedas al almacén de blob a través de un medio de almacenamiento compatible con el espacio de nombre (actualmente solo Memcache, el almacén de datos y la cola de tareas). Por ejemplo, si el Key de un blob se almacena en una entidad de almacén de datos, puedes acceder a él con un Key o un Query de almacén de datos que tenga en cuenta el espacio de nombres.

Si la aplicación accede al almacén de blob mediante claves almacenadas en un medio de almacenamiento compatible con los espacios de nombre, no es necesario segmentar el almacén de blob por espacio de nombre. Las aplicaciones deben evitar las pérdidas de blobs entre los espacios de nombre. Para ello, deben:

  • No se usa BlobInfo.gql() para las solicitudes de los usuarios finales. Puedes utilizar consultas BlobInfo para solicitudes administrativas (por ejemplo, para generar informes sobre todos los blobs de aplicaciones), pero si las empleas para solicitudes de usuarios finales, puedes provocar pérdidas de datos porque no todos los registros de BlobInfo están divididos en distintas partes por espacio de nombre.
  • Evitar el uso de claves del almacén de blob que procedan de fuentes no fiables.

Configurar espacios de nombres para consultas de Datastore

En la consola de Google Cloud , puedes definir el espacio de nombres de las consultas de Datastore.

Si no quieres usar el predeterminado, selecciona el espacio de nombres que quieras usar en el menú desplegable.

Usar espacios de nombres con el cargador masivo

El cargador masivo admite la marca --namespace=NAMESPACE, que le permite especificar el espacio de nombres que quiere usar. Cada espacio de nombre se administra de forma individual y, si quieres acceder a todos los espacios de nombre, deberás iterarlos.

Cuando creas una instancia de Index, se le asigna el espacio de nombres actual de forma predeterminada:

# set the current namespace
namespace_manager.set_namespace("aSpace")
index = search.Index(name="myIndex")
# index namespace is now fixed to "aSpace"

También puedes asignar un espacio de nombres explícitamente en el constructor:

index = search.Index(name="myIndex", namespace="aSpace")

Una vez que hayas creado un objeto IndexSpec, no podrás cambiar su espacio de nombres:

# change the current namespace
namespace_manager.set_namespace("anotherSpace")
# the namespaceof 'index' is still "aSpace" because it was bound at create time
index.search('hello')