Projeto de fábrica em cache

9

Eu tenho uma fábrica class XFactoryque cria objetos de class X. Como as instâncias Xsão muito grandes, o objetivo principal da fábrica é armazená-las em cache, da maneira mais transparente possível para o código do cliente. Como os objetos class Xsão imutáveis, o código a seguir parece razoável:

# 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:
  # ...

É um bom padrão? (Eu sei que não é o padrão de fábrica real.) Há algo que eu deva mudar?

Agora, acho que às vezes quero armazenar em cache Xobjetos no disco. Usarei picklepara esse fim e armazenarei como valores nos _registrynomes dos arquivos dos objetos em conserva, em vez de referências aos objetos. Obviamente, _registryele próprio teria que ser armazenado persistentemente (talvez em um arquivo pickle próprio, em um arquivo de texto, em um banco de dados ou simplesmente fornecendo aos arquivos pickle os nomes de arquivos que contêm hash_id).

Exceto agora, a validade do objeto em cache depende não apenas dos parâmetros passados ​​para get_x(), mas também da versão do código que criou esses objetos.

A rigor, mesmo um objeto armazenado em cache na memória pode se tornar inválido se alguém modificar x.pyou qualquer uma de suas dependências e recarregá-lo enquanto o programa estiver em execução. Até agora, eu ignorei esse perigo, pois parece improvável para minha aplicação. Mas certamente não posso ignorá-lo quando meus objetos são armazenados em cache para armazenamento persistente.

O que eu posso fazer? Suponho que eu poderia tornar o hash_idmais robusto calculando o hash de uma tupla que contém argumentos arg1e arg2, assim como o nome do arquivo e a data da última modificação de x.pytodos os módulos e arquivos de dados dos quais depende (recursivamente). Para ajudar a excluir arquivos de cache que nunca mais serão úteis, eu adicionaria à _registryrepresentação unilateral das datas modificadas para cada registro.

Mas mesmo essa solução não é 100% segura, pois teoricamente alguém pode carregar um módulo dinamicamente, e eu não saberia disso analisando estaticamente o código-fonte. Se eu me esforçar ao máximo e assumir que todos os arquivos do projeto são dependentes, o mecanismo ainda será interrompido se algum módulo pegar dados de um site externo, etc.).

Além disso, a frequência de alterações x.pye suas dependências é bastante alta, levando à invalidação pesada do cache.

Portanto, imaginei que seria melhor desistir de alguma segurança e invalidar o cache apenas quando houver uma incompatibilidade óbvia. Isso significa que class Xteria um identificador de validação de cache no nível de classe que deve ser alterado sempre que o desenvolvedor acreditar que ocorreu uma alteração que deve invalidar o cache. (Com vários promotores, um identificador de invalidação separado é necessário para cada um.) Este identificador é hash juntamente com arg1e arg2e torna-se parte das chaves hash armazenado na _registry.

Como os desenvolvedores podem esquecer de atualizar o identificador de validação ou não perceber que invalidaram o cache existente, seria melhor adicionar outro mecanismo de validação: class Xpode haver um método que retorne todos os "traços" conhecidos de X. Por exemplo, se Xfor uma tabela, posso adicionar os nomes de todas as colunas. O cálculo de hash também incluirá as características.

Posso escrever esse código, mas receio que esteja perdendo algo importante; e também estou me perguntando se talvez já exista uma estrutura ou pacote que possa fazer tudo isso. Idealmente, eu gostaria de combinar cache na memória e em disco.

EDITAR:

Pode parecer que minhas necessidades possam ser bem atendidas por um padrão de piscina. Em uma investigação mais aprofundada, no entanto, não é o caso. Eu pensei em listar as diferenças:

  1. Um objeto pode ser usado por vários clientes?

    • Pool: Não, cada objeto precisa ser retirado e depois verificado quando não é mais necessário. O mecanismo preciso pode ser complicado.
    • XFactory: Sim. Os objetos são imutáveis ​​e podem ser usados ​​por infinitos clientes ao mesmo tempo. Nunca é necessário criar uma segunda cópia do mesmo objeto.
  2. O tamanho da piscina precisa ser controlado?

    • Piscina: Frequentemente, sim. Nesse caso, a estratégia para fazer isso pode ser bastante complicada.
    • XFactory: Não. Um objeto deve ser entregue sob demanda ao cliente e, se um objeto existente for inadequado, será necessário criar um novo.
  3. Todos os objetos são livremente substituíveis?

    • Pool: Sim, os objetos geralmente são livremente substituíveis (ou, se não, é trivial verificar qual objeto o cliente precisa).
    • XFactory: Absolutamente não, e é muito difícil descobrir se um determinado objeto pode atender a uma determinada solicitação do cliente. Depende da disponibilidade de um objeto existente criado com (a) os mesmos argumentos e (b) com a mesma versão do código-fonte. A parte (b) não pode ser verificada pelo XFactory, portanto, solicita ao cliente que o ajude. O cliente cumpre essa responsabilidade de duas maneiras. Primeiro, o cliente pode incrementar qualquer um dos vários contadores de versão internos designados (um por desenvolvedor). Isso não pode acontecer no tempo de execução, apenas um desenvolvedor pode alterar esses contadores quando acredita que a alteração no código-fonte torna inutilizáveis ​​os objetos existentes. Segundo, um cliente retornará alguns invariantes sobre os objetos necessários e o XFactory verificará se esses invariantes não foram violados antes de servir o objeto ao cliente. Se alguma dessas verificações falhar,
  4. O impacto no desempenho precisa de uma análise cuidadosa?

    • Pool: Sim, em alguns casos, um pool realmente prejudica o desempenho se a sobrecarga do gerenciamento de objetos for maior que a sobrecarga da criação / destruição de objetos.
    • XFactory: Não. Sabe-se que os custos de computação dos objetos em questão são muito altos, e carregá-los da memória ou do disco é sem dúvida superior ao recalculá-los do zero.
  5. Quando os objetos são destruídos?

    • Piscina: quando a piscina está fechada. Talvez também possa destruir objetos se for solicitado (parcialmente) a liberar recursos ou se determinados objetos não forem usados ​​há algum tempo.
    • XFactory: sempre que um objeto foi criado com a versão do código-fonte que não é mais atual, como evidenciado por violação invariável ou contradição de correspondência. O processo de localizar e destruir esses objetos no momento certo é bastante complicado. Além disso, a invalidação baseada em tempo de todos os objetos pode ser implementada para reduzir os riscos acumulados do uso de objetos inválidos. Como o XFactory nunca tem certeza de que é o único proprietário de um objeto, essa invalidação é melhor alcançada por um "contador de versão" adicional nos objetos do cliente, que é incrementado programaticamente periodicamente, e não por um desenvolvedor.
  6. Que considerações especiais existem para o ambiente multithread?

    • Pool: precisa evitar colisões no check-in / check-in de objetos (não deseja fazer check-out de um objeto para dois clientes)
    • XFactory: precisa evitar colisões na criação de objetos (não deseja criar dois objetos com base em duas solicitações idênticas)
  7. O que precisa ser feito se o cliente não liberar um objeto?

    • Pool: pode ser necessário disponibilizar o objeto para outras pessoas depois de esperar algum tempo.
    • XFactory: Não aplicável. Os clientes não notificam o XFactory sobre quando são concluídos com o objeto.
  8. Os objetos precisam ser modificados?

    • Pool: talvez seja necessário redefinir o estado padrão antes de ser reutilizado.
    • XFactory: Não, os objetos são imutáveis.
  9. Existem considerações especiais relacionadas à persistência de objetos?

    • Piscina: Normalmente não. Um pool é sobre como economizar o custo de criação de objetos, para que todos os objetos sejam mantidos na memória (a leitura do disco anularia a finalidade).
    • XFactory: Sim, o XFactory é sobre economizar o custo de realizar cálculos complexos, portanto, faz sentido armazenar objetos pré-calculados no disco. Como resultado, o XFactory precisa lidar com os problemas típicos do armazenamento persistente; por exemplo, na inicialização, ele precisa se conectar ao armazenamento persistente, obter dele os metadados sobre quais objetos estão disponíveis no momento e estar pronto para carregá-los na memória, se solicitado. E o objeto pode estar em um dos três estados: "não existe", "existe no disco", "existe na memória". Enquanto o XFactory está em execução, o estado pode mudar apenas em uma direção (à direita nesta sequência).

Em resumo, a complexidade do pool está nos itens 1, 2, 4, 6 e possivelmente 5, 7, 8. A complexidade do XFactory está nos itens 3, 6, 9. A única sobreposição é o item 6, e realmente não é o núcleo. função de pool ou XFactory, mas sim uma restrição no design comum a qualquer padrão que precise funcionar em um ambiente multithread.

max
fonte
1
Definitivamente, essa não é uma fábrica ou mesmo perto de verdade. A fábrica trata da indireção da construção, permitindo que um tipo concreto seja criado a partir de uma especificação abstrata. Isto é uma piscina. Não é ruim por si só, mas agora que você sabe que é um pool de objetos que você está procurando, sugiro ler as boas práticas com pools e procurar advertências que as pessoas aprenderam a evitar e como reimplementar os problemas que eles ' eu sofri. Comece aqui: pt.wikipedia.org/wiki/Object_pool_pattern
Jimmy Hoffa
1
Obrigado. Achei essa leitura muito útil, mas o que preciso não é exatamente o padrão da piscina. Eu editei minha pergunta para mostrar o porquê.
max

Respostas:

4

Suas preocupações são muito válidas e elas me dizem que sua solução original de armazenamento em cache fácil está se tornando parte de sua arquitetura, o que naturalmente traz um novo nível de problemas conforme você se descreveu.

Uma boa solução arquitetural para armazenamento em cache é usar anotações combinadas com IoC, que resolvem vários problemas que você descreveu. Por exemplo:

  • Permite controlar melhor o ciclo de vida de seus objetos em cache
  • Permite substituir o comportamento do cache facilmente, alterando as anotações (em vez de alterar a implementação)
  • Permite configurar facilmente um cache de várias camadas, onde você pode armazenar na memória e, em seguida, o cache do disco, por exemplo
  • Permite definir a chave (hash) para cada método na própria anotação

Nos meus projetos (Java ou C #), uso anotações em cache do Spring. Você pode encontrar uma breve descrição aqui .

IoC é um conceito-chave nesta solução, pois permite que você configure seu sistema de armazenamento em cache da maneira que desejar.

Para implementar uma solução semelhante no Python, você precisa descobrir como usar anotações e procurar um contêiner de IoC que permita criar proxies. É assim que as anotações funcionam para interceptar todas as chamadas de método e fornecer a você essa solução específica para armazenamento em cache.

Alex
fonte
Obrigado, nunca ouvi falar de IoC até agora, e parece ser interessante e relevante. Parece haver alguns bons exemplos de IoC em Python.
max
@max IoC provavelmente não é um problema. Mas, para ter uma boa estrutura de armazenamento em cache, você deve encontrar uma maneira de interceptar chamadas de método (geralmente com proxies automáticos) e usar a anotação para implementar o comportamento de armazenamento em cache desejado. Boa sorte!
Alex13 /
1

Do jeito que eu vejo, o cache está bom - o X não está.

A desserialização de IMHO de instâncias únicas não deve ser um problema do cache. É uma tarefa para a classe de acordo. O principal problema aqui é que essa classe está mudando frequentemente. Sugiro separar a preocupação de armazenar em cache as instâncias e a preocupação de desserializar o objeto. O último precisa ser aprimorado para que o X possa desserializar formatos antigos também. Isso pode ser muito complicado e caro. Se for muito caro, você deve se perguntar se realmente precisa carregar versões antigas, desde que o X mude com frequência?

BTW, um identificador de versão parece obrigatório. Sem um conhecimento adicional da estrutura de XI, apenas podemos fazer algumas suposições, mas a estrutura de X parece ser logicamente modular (por exemplo, você falou de características). Nesse caso, talvez ajude a tornar essa estrutura explícita.

scarfridge
fonte