Diseño de fábrica de almacenamiento en caché

9

Tengo una fábrica class XFactoryque crea objetos de class X. Las instancias de Xson muy grandes, por lo que el objetivo principal de la fábrica es almacenarlas en caché, de la forma más transparente posible al código del cliente. Los objetos de class Xson inmutables, por lo que el siguiente código parece razonable:

# module xfactory.py
import x
class XFactory:
  _registry = {}

  def get_x(self, arg1, arg2, use_cache = True):
    if use_cache:
      hash_id = hash((arg1, arg2))
      if hash_id in _registry:
        return _registry[hash_id]
    obj = x.X(arg1, arg2)
    _registry[hash_id] = obj
    return obj

# module x.py
class X:
  # ...

¿Es un buen patrón? (Sé que no es el patrón de fábrica real). ¿Hay algo que deba cambiar?

Ahora, encuentro que a veces quiero almacenar en caché los Xobjetos en el disco. Usaré picklepara ese propósito y almacenaré como valores en _registrylos nombres de archivo de los objetos en escabeche en lugar de referencias a los objetos. Por supuesto, _registrysí mismo tendría que almacenarse de forma persistente (tal vez en un archivo pickle propio, en un archivo de texto, en una base de datos, o simplemente dando a los archivos pickle los nombres de archivo que contienen hash_id).

Excepto que ahora la validez del objeto en caché depende no solo de los parámetros pasados get_x(), sino también de la versión del código que creó estos objetos.

Estrictamente hablando, incluso un objeto almacenado en la memoria caché podría volverse inválido si alguien modifica x.pyo cualquiera de sus dependencias y lo recarga mientras el programa se está ejecutando. Hasta ahora ignoré este peligro, ya que parece poco probable para mi aplicación. Pero ciertamente no puedo ignorarlo cuando mis objetos se almacenan en caché en un almacenamiento persistente.

¿Que puedo hacer? Supongo que podría hacer hash_idmás robusto calculando el hash de una tupla que contiene argumentos arg1y arg2, así como el nombre de archivo y la última fecha de modificación para x.pycada módulo y archivo de datos del que depende (recursivamente). Para ayudar a eliminar archivos de caché que nunca volverán a ser útiles, agregaría a la _registryrepresentación sin compartir de las fechas modificadas para cada registro.

Pero incluso esta solución no es 100% segura, ya que teóricamente alguien podría cargar un módulo dinámicamente, y no lo sabría al analizar estáticamente el código fuente. Si hago todo lo posible y asumo que cada archivo en el proyecto es una dependencia, el mecanismo aún se romperá si algún módulo toma datos de un sitio web externo, etc.).

Además, la frecuencia de los cambios x.pyy sus dependencias es bastante alta, lo que lleva a una gran invalidación de caché.

Por lo tanto, pensé que también podría renunciar a un poco de seguridad, e invalidar el caché solo cuando haya un desajuste evidente. Esto significa que class Xtendría un identificador de validación de caché de nivel de clase que debería cambiarse siempre que el desarrollador crea que se produjo un cambio que debería invalidar la caché. (Con múltiples desarrolladores, se requiere un identificador de invalidación por separado para cada.) Este identificador es ordenado junto con arg1y arg2y se convierte en parte de las claves hash almacenados en _registry.

Dado que los desarrolladores pueden olvidarse de actualizar el identificador de validación o no darse cuenta de que invalidaron la memoria caché existente, parece mejor agregar otro mecanismo de validación: class Xpuede tener un método que devuelva todos los "rasgos" conocidos de X. Por ejemplo, si Xes una tabla, podría agregar los nombres de todas las columnas. El cálculo de hash también incluirá los rasgos.

Puedo escribir este código, pero me temo que me falta algo importante; y también me pregunto si quizás hay un marco o paquete que ya puede hacer todo esto. Idealmente, me gustaría combinar el almacenamiento en caché en memoria y en caché.

EDITAR:

Puede parecer que mis necesidades pueden ser bien atendidas por un patrón de piscina. En investigaciones posteriores, sin embargo, no es el caso. Pensé en enumerar las diferencias:

  1. ¿Puede un objeto ser utilizado por múltiples clientes?

    • Grupo: No, cada objeto debe ser extraído y luego registrado cuando ya no sea necesario. El mecanismo preciso puede ser complicado.
    • XFactory: sí. Los objetos son inmutables y pueden ser utilizados por infinitos clientes a la vez. Nunca es necesario crear una segunda copia del mismo objeto.
  2. ¿Es necesario controlar el tamaño de la piscina?

    • Piscina: a menudo sí. Si es así, la estrategia para hacerlo puede ser bastante complicada.
    • XFactory: No. Se debe entregar un objeto a pedido del cliente, y si un objeto existente no es adecuado, se debe crear uno nuevo.
  3. ¿Todos los objetos son libremente sustituibles?

    • Grupo: Sí, los objetos suelen ser de libre sustitución (o, de lo contrario, es trivial verificar qué objeto necesita el cliente).
    • XFactory: Absolutamente no, y es muy difícil averiguar si un objeto determinado puede atender una solicitud de cliente determinada. Depende de si un objeto existente está disponible que se creó con (a) los mismos argumentos y (b) la misma versión del código fuente. XFactory no puede verificar la parte (b), por lo que le pide ayuda al cliente. El cliente cumple con esta responsabilidad de dos maneras. Primero, el cliente puede incrementar cualquiera de sus varios contadores de versión interna designados (uno por desarrollador). Esto no puede suceder en tiempo de ejecución, solo un desarrollador puede cambiar estos contadores cuando cree que el cambio del código fuente hace que los objetos existentes sean inutilizables. En segundo lugar, un cliente devolverá algunos invariantes sobre los objetos que necesita, y XFactory verificará que estos invariantes no sean violados antes de entregar el objeto al cliente. Si alguno de estos controles falla,
  4. ¿El impacto en el rendimiento necesita un análisis cuidadoso?

    • Pool: Sí, en algunos casos, un pool realmente perjudica el rendimiento si la sobrecarga de la gestión de objetos es mayor que la sobrecarga de creación / destrucción de objetos.
    • XFactory: No. Se sabe que los costos de cómputo de los objetos en cuestión son muy altos, y cargarlos desde la memoria o desde el disco es sin duda superior a volver a calcularlos desde cero.
  5. ¿Cuándo se destruyen los objetos?

    • Piscina: cuando la piscina está cerrada. Quizás también podría destruir objetos si se le ordena liberar (parcialmente) recursos o si ciertos objetos no se han utilizado durante un tiempo.
    • XFactory: cada vez que se crea un objeto con la versión del código fuente que ya no es actual, como lo demuestra la violación invariante o la falta de coincidencia del contador. El proceso de localizar y destruir tales objetos en el momento adecuado es bastante complicado. Además, la invalidación basada en el tiempo de todos los objetos puede implementarse para reducir los riesgos acumulados de usar objetos no válidos. Dado que XFactory nunca está seguro de que sea el único propietario de un objeto, tal invalidación se logra mejor mediante un "contador de versiones" adicional en los objetos del cliente, que se incrementa programáticamente de forma periódica, en lugar de un desarrollador.
  6. ¿Qué consideraciones especiales existen para el entorno multiproceso?

    • Pool: tiene que evitar colisiones en la extracción / comprobación de objetos (no desea extraer un objeto a dos clientes)
    • XFactory: tiene que evitar la colisión en la creación de objetos (no desea crear dos objetos basados ​​en dos solicitudes idénticas)
  7. ¿Qué debe hacerse si el cliente no libera un objeto?

    • Grupo: puede querer que el objeto esté disponible para otros después de esperar un tiempo.
    • XFactory: no aplicable. Los clientes no notifican a XFactory sobre cuándo terminan con el objeto.
  8. ¿Los objetos necesitan ser modificados?

    • Grupo: puede que sea necesario restablecer el estado predeterminado antes de volver a utilizarlo.
    • XFactory: No, los objetos son inmutables.
  9. ¿Hay alguna consideración especial relacionada con la persistencia de los objetos?

    • Piscina: normalmente no. Un grupo consiste en ahorrar el costo de la creación de objetos, por lo que todos los objetos se guardan en la memoria (la lectura del disco anularía el propósito).
    • XFactory: Sí, XFactory se trata de ahorrar el costo de realizar cálculos complejos, por lo que tiene sentido almacenar objetos precalculados en el disco. Como resultado, XFactory necesita lidiar con los problemas típicos del almacenamiento persistente; por ejemplo, en la inicialización, necesita conectarse al almacenamiento persistente, obtener de él los metadatos sobre qué objetos están disponibles actualmente allí y estar listo para cargarlos en la memoria si así se solicita. Y el objeto puede estar en uno de tres estados: "no existe", "existe en el disco", "existe en la memoria". Mientras XFactory se está ejecutando, el estado puede cambiar solo en una dirección (a la derecha en esta secuencia).

En resumen, la complejidad del grupo está en los elementos 1, 2, 4, 6 y posiblemente 5, 7, 8. La complejidad de XFactory está en los elementos 3, 6, 9. La única superposición es el elemento 6, y realmente no es el núcleo función de grupo o XFactory, sino más bien una restricción en el diseño que es común a cualquier patrón que necesite funcionar en un entorno multiproceso.

max
fuente
1
Definitivamente, esto no es una fábrica o incluso está cerca. Factory trata sobre la indirección de la construcción permitiendo que se cree un tipo concreto a partir de una especificación abstracta. Esta es una piscina. No es malo per se, pero ahora que sabe que es un conjunto de objetos lo que está buscando, le sugiero que lea sobre buenas prácticas con los conjuntos y busque las advertencias que la gente ha aprendido a evitar y cómo evitar volver a implementar los problemas que '' d sufrió. Comience aquí: en.wikipedia.org/wiki/Object_pool_pattern
Jimmy Hoffa
1
Gracias. Esta lectura me pareció muy útil, pero lo que necesito no es el patrón de grupo. Edité mi pregunta para mostrar por qué.
max

Respuestas:

4

Sus inquietudes son muy válidas y me dicen que su solución original de almacenamiento en caché fácil eventualmente se está convirtiendo en parte de su arquitectura, lo que naturalmente trae un nuevo nivel de problemas como usted mismo describió.

Una buena solución arquitectónica para el almacenamiento en caché es usar anotaciones combinadas con IoC que resuelve varios problemas que usted describió. Por ejemplo:

  • Le permite controlar mejor el ciclo de vida de sus objetos en caché
  • Le permite reemplazar fácilmente el comportamiento de almacenamiento en caché cambiando las anotaciones (en lugar de cambiar la implementación)
  • Le permite configurar fácilmente un caché de varias capas donde podría almacenar en la memoria y luego en el caché del disco, por ejemplo
  • Le permite definir la clave (hash) para cada método en la anotación misma

En mis proyectos (Java o C #) utilizo anotaciones de almacenamiento en caché Spring. Puede encontrar una breve descripción aquí .

IoC es un concepto clave en esta solución porque le permite configurar su sistema de almacenamiento en caché de la forma que desee.

Para implementar una solución similar en Python, debe encontrar cómo usar las anotaciones y buscar un contenedor de IoC que le permita construir proxies. Así es como funcionan las anotaciones para interceptar todas las llamadas a métodos y proporcionarle esta solución particular para el almacenamiento en caché.

Alex
fuente
Gracias, nunca he oído hablar de IoC hasta ahora, y parece ser interesante y relevante. Parece que hay algunos buenos ejemplos de IoC en Python.
max
@max IoC probablemente no sea tan importante. Pero para tener un buen marco de almacenamiento en caché, debe encontrar una manera de interceptar las llamadas a métodos (generalmente con proxys automáticos) y luego usar la anotación para implementar el comportamiento de almacenamiento en caché que desee. ¡Buena suerte!
Alex
1

A mi modo de ver, el caché está bien, X no.

En mi humilde opinión, la deserialización de instancias individuales no debería ser un problema de la memoria caché. Es una tarea para la clase correspondiente. El problema principal aquí es que esta clase está cambiando con frecuencia. Sugiero separar la preocupación de las instancias de almacenamiento en caché y la preocupación de deserializar el objeto. Este último tiene que mejorarse para que X también pueda des serializar formatos más antiguos. Esto puede ser muy complicado y costoso. Si es demasiado costoso, debe preguntarse si realmente necesita cargar versiones antiguas siempre que X cambie con frecuencia.

Por cierto, un identificador de versión parece obligatorio. Sin un mayor conocimiento de la estructura de XI solo puedo hacer algunas conjeturas, pero la estructura de X parece ser lógicamente modular (por ejemplo, usted habló de rasgos). Si es así, tal vez sería útil hacer explícita esta estructura.

Scarfridge
fuente