Skip to content

Instantly share code, notes, and snippets.

@mgaitan
Last active June 20, 2026 19:02
Show Gist options
  • Select an option

  • Save mgaitan/f73e28feb3219efe6e59ec665507cfde to your computer and use it in GitHub Desktop.

Select an option

Save mgaitan/f73e28feb3219efe6e59ec665507cfde to your computer and use it in GitHub Desktop.
Fierro: marshalling, mutabilidad y caché — diagnóstico y plan de refactor

Marshalling, mutabilidad y caché en Fierro: diagnóstico y plan de refactor

Complementa Arquitectura. Referencias al código apuntan a develop en GitHub.


Parte 1 — El sistema de marshalling

1.1 Por qué existe

Fierro es una app cliente/servidor donde el cliente desktop (wxPython) y el webclient (Flask) invocan funciones en el server. Los parámetros y retornos son objetos Python del dominio (Book, Provider, SaleBill, …) que ningún protocolo de red estándar puede serializar directamente.

El marshalling convierte esos objetos a tipos primitivos (listas, dicts, strings, números) para transportarlos y los reconstruye del lado receptor. Sin él, habría que picar esa serialización a mano para cada tipo y cada método — exactamente lo que hace el sistema, pero generado automáticamente desde XML.

1.2 La arquitectura XML-driven

Hay una fuente de verdad en XML desde la que se genera código Python:

colofon/server/services.xml              ← contrato de métodos remotos
colofon/client/ipc/modulesConfig.xml     ← mapa clase-XML → factory Python (cliente)
colofon/server/ipc/modulesConfig.xml     ← idem lado servidor
colofon/client/ipc/lazyClasses.xml       ← clases de referencia lazy
         │
         ▼  inv make-marshalling  (makeMarshalling.sh dentro del container)
fierrotools/objectTransfer/
    generateMarshallings.py    ← genera *Marshalling.py
    generateServices.py        ← genera services.py
         │
         ▼
colofon/client/ipc/*Marshalling.py   ← generados, .gitignore, NO editar
colofon/server/ipc/*Marshalling.py   ← generados, .gitignore, NO editar
colofon/client/ipc/services.py       ← generado
colofon/server/ipc/services.py       ← generado
colofon/client/ipc/lazyClasses.py    ← generado
colofon/server/ipc/lazyClasses.py    ← generado

Si se cambia una clase de dominio o la firma de un método sin regenerar, cliente y server quedan fuera de sincronía silenciosamente.

1.3 Tres modos de transferir objetos

Cada objeto puede viajar de tres formas según lo que necesite el método:

flowchart TD
    obj["Objeto dominio\n(ej: Book con 50 attrs)"]

    obj --> thin["**ThinRef**\nSolo clave primaria\n─────────────────\nmarshall: identityFn(obj.id)\nunmarshall: BookThinRef(id=…)\nLazy load al primer acceso"]

    obj --> gross["**GrossRef**\nSolo atributos cargados\nen __dict__\n─────────────────\nmarshall: {k:v for k in obj.__dict__}\nunmarshall: BookGrossRef(**dict)\nEstado parcial posible"]

    obj --> byval["**ByValue**\nTodos los atributos\ndefinidos en el XML\n─────────────────\nmarshall: [attr1, attr2, …]\nunmarshall: Book(attr1=…, …)\nSnapshot completo, seguro"]
Loading

ThinRef: transporta solo la PK. El receptor crea un stub que llama al manager la primera vez que se accede a cualquier atributo (ver lazyClasses.py).

GrossRef: transporta solo los atributos que están en obj.__dict__ al momento de serializar. No garantiza completitud — es el modo más peligroso para el caché.

ByValue: transporta todos los atributos declarados en el XML. Es el modo seguro y explícito; usado para objetos editados que van del cliente al server.

1.4 El ciclo completo de una llamada remota

sequenceDiagram
    participant C as Cliente (wxPython)
    participant SM as services.py (cliente)
    participant SP as serverProxy
    participant DS as Dispatcher (server)
    participant MM as Manager (server)

    C->>SM: BookManager.findByPrimaryKey(42)
    note over SM: services["/book"].methods["findByPrimaryKey"]
    SM->>SM: parámetro marshallado: 42 → 42 (identityFn)
    SM->>SP: run("/book", "findByPrimaryKey", [42])
    SP->>DS: POST /run/book/findByPrimaryKey/ {params:[42]}
    DS->>MM: manager.findByPrimaryKey(42)
    MM-->>DS: Book(id=42, title="…", …)
    DS->>DS: marshallBookGrossRef(book) → {"id":42,"title":"…",…}
    DS-->>SP: {"id":42,"title":"…",…}
    SP->>SM: unmarshallBookGrossRef({"id":42,…})
    SM-->>C: BookGrossRef(id=42, title="…")
Loading

Cada llamada involucra cuatro conversiones (marshal params + marshal retorno + unmarshal retorno) definidas por el XML.

1.5 Cómo juega Pyro4

Pyro4 es el framework RPC que maneja la capa de transporte. En Fierro usa msgpack como serializador de red:

# colofon/client/ipc/serverProxy.py
Pyro4.config.SERIALIZER = "msgpack"
Pyro4.config.MAX_MESSAGE_SIZE = 1024 * 1024 * 1024  # 1 GB — monkeypatch necesario

Pyro no conoce las clases de dominio. Solo serializa los primitivos que el marshalling produce. El flujo real es:

Objeto dominio → marshallFn → dict/list de primitivos → msgpack → bytes → red
                                                              ↑
                                                  Pyro4 serializa esto

Problemas con Pyro4 hoy:

1.6 Los obstáculos con HTTP

HTTPServerProxy reimplementa la misma interfaz sobre requests. flaskServer.py recibe los requests y llama al mismo dispatcher.

Lo que funciona bien: cliente y server comparten el mismo dispatcher, la lógica de negocio es idéntica en ambos modos.

Los obstáculos específicos de HTTP:

Problema Cómo se manifiesta
Excepciones Pyro las reconstruye automáticamente; HTTP las serializa como {"error":"ValidationError","description":"…"} y el cliente las reconstruye a mano (serverProxy.py:162)
Tipos especiales Report y File tienen marshall explícito en pack() (flaskServer.py:57); agregar un tipo nuevo requiere tocar ese punto
Sesión Pyro la pasa por contexto del proxy; HTTP la embebe en la URI (?sessionId=) o en header Authorization: SessionID 123
Sin push Pyro soporta callbacks cliente←server; HTTP no, entonces updates en tiempo real requieren polling
Payload grande Mensajes de 50 MB se entregan de una vez; no hay streaming chunked

Parte 2 — Mutabilidad, caché y los bugs

2.1 El contrato implícito roto

El sistema asume un contrato que el código no cumple:

"Un objeto que viene del server es un snapshot del estado en ese instante. Una vez unmarshalled, no debería cambiar."

En la práctica los objetos son mutables y se modifican por tres razones:

  1. GrossRef lazy loading: BookGrossRef carga atributos on-demand llamando al manager. El mismo objeto Python puede tener distintos atributos en distintos momentos.
  2. Write-through en caché: antes de PR #4166 (ticket 49032), al actualizar un objeto el código escribía el objeto editado directamente en el caché. Si el objeto editado estaba incompleto, el caché quedaba con datos parciales.
  3. Compartición de referencias: el GrossRef en caché y el que usa la UI son el mismo objeto. Una modificación en la pantalla de edición contamina el caché.

2.2 Las capas de caché y cómo se superponen

Hay cuatro capas de caché independientes que no coordinan entre sí:

block-beta
  columns 1

  block:CLIENT["🖥️  CLIENTE"]:1
    A["CacheWrapper\n(initManagers.py)\nstrongDict: {pk → GrossRef}  ← MUTABLE\nInvalidación total en mutaciones"]
    B["lru_unmarshallWithRefs\n(objectTransferLib.py:481)\nEvita duplicar instancias del mismo GrossRef\nActualiza __dict__ de refs ya existentes"]
    C["cache_per_request\n(fierrotools/cache.py)\nCache local por thread/request\nSe limpia entre pantallas"]
  end

  block:SERVER["🖧  SERVER"]:1
    D["FullCacheDbObjectPool\n(server/model/utils/cache.py)\n__cache: {pk → objeto server}  ← MUTABLE\nInvalidación total o por clave desde PR #4166"]
  end
Loading

Cada capa fue agregada independientemente para resolver un problema de performance puntual, sin un modelo de coherencia global.

2.3 La cadena de bugs documentada

PR #3775 / Sentry FIERRO-SERVER-2N: PartyNotesEditionConfig llamaba manager._invalidate() de forma ciega. CacheReuseWrapper.__getattr__ captura cualquier nombre de método y lo delega al manager subyacente — incluyendo _invalidate. Para SalesmanManager, que no tiene _invalidate localmente, el proxy terminaba enviando una llamada remota al server con method="_invalidate". El server tiraba AttributeError.

# El problema en CacheReuseWrapper.__getattr__
def __getattr__(self, attName):
    if attName in self.invalidateMethods:
        self._invalidate()
    return getattr(self.manager, attName)  # ← si no existe localmente,
                                           #   llama al manager REMOTO

PR #4166 (ticket 49032): al enviar documentos, la UI modificaba el objeto y hacía write-through al caché. El objeto modificado para el formulario (campos parciales) sobreescribía el objeto completo en caché. Fix: reemplazar write-through por drop_cached_item (invalidación puntual).

La causa raíz de ambos: los objetos del dominio son mutables, compartidos por referencia entre caché y UI, sin contrato explícito sobre quién puede modificarlos.


Parte 3 — Plan de refactor incremental

Los niveles están ordenados por esfuerzo/riesgo ascendente. Cada nivel es independiente del siguiente.

Nivel 0 — Mantenimiento defensivo (días)

0.1 Proteger __getattr__ en los wrappers de caché

El fix de PR #3775 fue puntual. Generalizarlo en CacheReuseWrapper:

def __getattr__(self, attName):
    if attName in self.invalidateMethods:
        if hasattr(type(self.manager), attName):  # solo si existe localmente
            self._invalidate()
    return getattr(self.manager, attName)

0.2 Tests de regresión de caché

PR #4166 agregó tests/unitarios/test_cache_wrapper.py. Ampliar con casos que verifiquen que modificar un objeto devuelto del caché no afecta la entrada del caché.

0.3 Completar el saneamiento del código generado (PR #4062 / #4449)

PR #4062 ("Ticket 3887: aplicar Ruff a codigo generado") agregó ruff check --fix-only al final de makeMarshalling.sh. Quedó incompleto: E721 (type(value) == dict) no es auto-fixable por ruff, dejando 8.413 violations en los *Marshalling.py generados.

La raíz estaba en el generador generateMarshallings.py: emitía type(value) == dict/list en genUnmarshallReference y genUnmarshallByValue. PR #4449 lo corrige cambiando el generador a isinstance()una línea en el generador → 0 E721 en todos los archivos generados. El objetivo es código generado limpio y compliance, no # noqa.


Nivel 1 — Low-hanging fruits (semanas)

1.1 Unificar services.py cliente/servidor

Actualmente existen colofon/client/ipc/services.py y colofon/server/ipc/services.py con contenido casi idéntico — mismas estructuras Service/Method, distintas referencias a funciones de marshall. El generador los produce por separado porque el módulo de import difiere (colofon.client.ipc vs colofon.server.ipc).

Alternativa: generar un colofon/ipc/services_schema.py con la estructura del contrato (sin funciones de marshall) y extender en cliente/server:

# colofon/ipc/services_schema.py  (generado — fuente de verdad del contrato)
SERVICE_SCHEMA = {
    "/book": {
        "findByPrimaryKey": {"params": [int], "return": "book.Book"},
        "queryItems":       {"params": [dict], "return": "list[book.Book]"},
    },
    ...
}

# colofon/client/ipc/services.py  (generado — usa client ipc marshalling)
from colofon.ipc.services_schema import SERVICE_SCHEMA
services = build_services(SERVICE_SCHEMA, ipc=colofon.client.ipc)

# colofon/server/ipc/services.py  (generado — usa server ipc marshalling)
from colofon.ipc.services_schema import SERVICE_SCHEMA
services = build_services(SERVICE_SCHEMA, ipc=colofon.server.ipc)

Esto no cambia el contrato en runtime, pero elimina la duplicación y hace auditable qué métodos existen desde un solo lugar.

1.2 Limpiar lazyClasses.py generado

lazyClasses.py genera cientos de pares ThinRef/GrossRef con makeRefClasses. Mientras se avanza hacia el nivel 3, agregar __slots__ en las clases generadas reduce el footprint de memoria de los GrossRef en caché.


Nivel 2 — Modernización moderada (1–3 meses)

2.1 Migrar de Pyro4 a HTTP como transporte principal

El modo HTTP ya existe y comparte el mismo dispatcher. Lo que falta para hacerlo equivalente:

  • Excepciones: centralizar en un ExceptionRegistry (ya hay base en colofon/model/exception.py) y usarlo simétricamente en pack()/unpack()
  • Sesión: unificar en header Authorization (ya funciona); eliminar el hack de ?sessionId= en la URI
  • Payload grande: Transfer-Encoding: chunked o multipart para reportes en lugar de un bloque único

Una vez equivalente, Pyro4 se puede deprecar sin tocar la lógica de negocio.

2.2 Objetos inmutables con Pydantic v2

El problema central de los bugs de caché es la mutabilidad. Los objetos del server deberían ser inmutables:

from pydantic import BaseModel, ConfigDict

class Book(BaseModel):
    model_config = ConfigDict(frozen=True)

    id: int
    title: str | None = None
    int_code: str | None = None
    prices: dict[str, "Price"] = {}

frozen=True hace que cualquier asignación tire ValidationError. Para edición, el cliente trabaja con una copia:

# En lugar de: book.title = "nuevo título"
book_editado = book.model_copy(update={"title": "nuevo título"})

Con objetos inmutables:

  • El caché puede guardar el objeto directamente sin riesgo de contaminación
  • lru_unmarshallWithRefs quedaría obsoleto (no tiene sentido actualizar un objeto cacheado)
  • Los tests se simplifican (sin efectos de borde por mutación compartida)

Dificultad: muchas partes del código cliente modifican los objetos directamente para los formularios. Hay que separar "objeto de lectura" (inmutable, del caché) de "objeto de edición" (mutable, local a la pantalla). Esto también es un beneficio de diseño.

2.3 Tipado completo del contrato IPC

Con el modelo basado en Pydantic, el IDE puede navegar y verificar tipos end-to-end. Se pueden generar stubs .pyi para que mypy verifique que el código cliente pasa el tipo correcto a cada método remoto:

# colofon/ipc/book_service.pyi  (generado o escrito a mano una vez)
class BookService:
    def find_by_primary_key(self, id: int) -> Book: ...
    def query_items(self, filter: dict[str, str]) -> list[Book]: ...

Esto elimina una clase entera de bugs donde se pasa el tipo equivocado y solo falla en runtime.


Nivel 3 — Rearquitectura profunda con pydantic-rpc (3–12 meses)

¿Qué es pydantic-rpc?

pydantic-rpc (MIT, activo al 2026-06-20, v0.15.2) es una librería que expone clases Python con métodos tipados como servicios ConnectRPC/gRPC sin escribir archivos protobuf. Genera el .proto en runtime a partir de las type annotations de Pydantic.

from pydantic_rpc import Message, Server

class BookId(Message):
    id: int

class Book(Message):
    id: int
    title: str | None = None

class BookService:
    def find_by_primary_key(self, request: BookId) -> Book:
        ...  # lógica de negocio

Server(BookService()).run()  # levanta servidor ConnectRPC/HTTP

Eso es todo del lado server. No hay XML, no hay generador de código, no hay makeMarshalling.sh.

Por qué aplica a Fierro

El problema central de Fierro es que el contrato cliente/server vive en XML y requiere un pipeline de generación frágil. pydantic-rpc resuelve exactamente ese problema: las firmas Python tipadas son el contrato, y la serialización la maneja Pydantic automáticamente.

Problema actual Solución con pydantic-rpc
XML como fuente de verdad Clases Python con type hints
*Marshalling.py generados × 2 Pydantic serializa/deserializa automáticamente
makeMarshalling.sh obligatorio No hay paso de generación
Duplicación cliente/server Un paquete de modelos compartido
Pyro4 obsoleto ConnectRPC (HTTP moderno, estándar abierto)
Excepciones custom gRPC status codes estándar

Protocolo: ConnectRPC sobre HTTP

ConnectRPC no es gRPC puro — es un protocolo HTTP/JSON compatible con browsers y con herramientas estándar:

# Una llamada ConnectRPC desde curl (sin cliente especial)
curl -X POST https://server/BookService/FindByPrimaryKey \
     -H "Content-Type: application/json" \
     -d '{"id": 42}'
# → {"id": 42, "title": "El nombre del viento", ...}

Esto significa que el cliente de Fierro (Python) puede ser tan simple como httpx + model_validate, sin generar stubs:

# colofon/client/ipc/book_client.py
import httpx
from colofon.ipc.models.book import Book, BookId

def find_by_primary_key(id: int, session_id: int) -> Book:
    r = httpx.post(
        f"{SERVER_URL}/BookService/FindByPrimaryKey",
        json={"id": id},
        headers={"connect-protocol-version": "1", "Authorization": f"SessionID {session_id}"},
    )
    return Book.model_validate(r.json())

Plan incremental

Paso 3.1 — Paquete de modelos compartido

Crear colofon/ipc/ como workspace member de uv (ya está configurado en pyproject.toml). Definir los modelos como pydantic_rpc.Message (que es BaseModel con frozen=True y algunas restricciones de serialización):

# colofon/ipc/models/book.py
from pydantic_rpc import Message

class BookId(Message):
    id: int

class Book(Message):
    id: int
    title: str | None = None
    int_code: str | None = None
    # ... campos con tipos explícitos, mismos que en el XML actual

class BookFilter(Message):
    filter: dict[str, str] = {}

class BookList(Message):
    items: list[Book]

Este paquete reemplaza las definiciones XML de modulesConfig.xml y los atributos en services.xml.

Paso 3.2 — Servicio server (un manager a la vez)

Crear un servicio que envuelve el manager existente sin tocar la lógica de DB:

# colofon/server/services/book_service.py
from colofon.ipc.models.book import Book, BookId, BookFilter, BookList
from colofon.server.model.catalogo.book import BookManagerPool

class BookService:
    def find_by_primary_key(self, request: BookId) -> Book:
        mgr = BookManagerPool().getManager()
        b = mgr.findByPrimaryKey(request.id)
        return Book.model_validate(b.__dict__)

    def query_items(self, request: BookFilter) -> BookList:
        mgr = BookManagerPool().getManager()
        books = mgr.queryItems(request.filter)
        return BookList(items=[Book.model_validate(b.__dict__) for b in books])

La lógica de negocio en los managers no cambia. Solo el adaptador de entrada/salida.

Paso 3.3 — Servidor ConnectRPC montado en Flask existente

pydantic-rpc genera una app ASGI. Se puede montar junto al Flask actual durante la transición:

# colofon/server/flaskServer.py  (existente, sin romper)
from pydantic_rpc import ConnectRPCApp
from colofon.server.services.book_service import BookService

connectrpc_app = ConnectRPCApp(BookService())
# montar en /rpc/  → los endpoints viejos siguen en /run/

Esto permite migrar servicio por servicio: el cliente puede llamar /run/book/findByPrimaryKey/ (viejo) o /rpc/BookService/FindByPrimaryKey (nuevo) en paralelo.

Paso 3.4 — Cliente httpx por servicio

Por cada servicio migrado, reemplazar el manager cliente (actualmente un wrapper sobre el proxy Pyro/HTTP) por un cliente directo:

# colofon/client/ipc/book_client.py
from colofon.ipc.models.book import Book, BookId, BookFilter, BookList

class BookClient:
    def findByPrimaryKey(self, id: int) -> Book:  # misma interfaz que el manager actual
        r = _http.post("/rpc/BookService/FindByPrimaryKey", json={"id": id})
        return Book.model_validate(r.json())

El resto del código cliente (colofon/client/model/, colofon/client/ui/) recibe un objeto Book de Pydantic donde antes recibía un BookGrossRef. La interfaz es compatible si los atributos coinciden.

Paso 3.5 — Eliminar el generador

Una vez todos los servicios migrados:

  • Borrar fierrotools/objectTransfer/
  • Borrar colofon/makeMarshalling.sh
  • Borrar colofon/*/ipc/*Marshalling.py del .gitignore (ya no existen)
  • Borrar colofon/*/ipc/services.py generados

Qué resuelve vs qué hay que resolver

✅ Lo que pydantic-rpc resuelve ⚠️ Lo que hay que resolver aparte
Elimina el generador XML Mapeo GrossRefBook frozen (ver 2.2)
Un contrato tipado y navegable Sesión: pasar sessionId como header en cada request
Serialización automática con Pydantic v2 Autenticación: interceptor gRPC o middleware Flask
Elimina Pyro4 Excepciones: mapear ValidationError a gRPC status
ConnectRPC es HTTP estándar (curl, browser) Streaming de reportes: usar AsyncIterator[T]
Migración incremental servicio a servicio Caché cliente: los objetos frozen son cacheables sin riesgo

Limitaciones conocidas de pydantic-rpc

  • Uniones con colecciones: Union[list[Book], None] no funciona directamente (restricción protobuf); usar BookList | None con wrapper
  • 77 estrellas: librería pequeña, puede tener rough edges en casos de borde
  • El .proto se genera en runtime: no se puede versionar ni distribuir directamente (es una ventaja para desarrollo, potencial limitación para interoperabilidad futura)
  • Campos nuevos: los serializadores de Pydantic que agregan campos nuevos son ignorados por protobuf

Comparación: pydantic-rpc vs FastAPI puro

Una alternativa más simple es FastAPI + httpx sin pydantic-rpc:

pydantic-rpc + ConnectRPC FastAPI + httpx
Definición del contrato Clase Python tipada (un lugar) Rutas FastAPI + modelos Pydantic (dos lugares)
Cliente httpx directo o stub generado httpx directo
Streaming AsyncIterator[T] nativo SSE o chunked manual
Interoperabilidad gRPC/ConnectRPC estándar (multi-lenguaje) REST/OpenAPI (más universal)
Complejidad Baja (un decorator, un Server()) Muy baja (decoradores FastAPI familiares)
Dependencias nuevas pydantic-rpc, grpcio solo fastapi (ya hay Flask)

Recomendación: si el objetivo es principalmente eliminar el generador y simplificar el stack Python-a-Python, FastAPI + httpx es más simple y más familiar. pydantic-rpc agrega valor si en el futuro se quieren clients en otros lenguajes o integración MCP con agentes de IA (lo cual pydantic-rpc soporta nativamente). Son dos caminos válidos; pydantic-rpc es el más ambicioso y el que más elimina de golpe.


Mapa de decisiones

Qué querés resolver Nivel
Bugs de caché inmediatos 0.1, 0.2
Reducir ruido en Sentry 0.1
Código generado ruff-compliant 0.3 → PR #4449
Reducir duplicación cliente/server 1.1
Eliminar Pyro4 2.1
Eliminar bugs de mutabilidad 2.2
Tipado completo end-to-end 2.3
Eliminar el generador XML y Pyro4 de raíz 3 (pydantic-rpc o FastAPI+httpx)
Interoperabilidad multi-lenguaje / MCP 3 con pydantic-rpc
Migración incremental servicio a servicio 3 (pasos 3.1–3.5)

Apéndice: flujo actual vs flujo objetivo

Flujo actual

XML (services.xml, modulesConfig.xml)
    ↓  inv make-marshalling  (bash → Python en container Docker)
*Marshalling.py × 2 (client + server, gitignored)
services.py × 2 (client + server)
lazyClasses.py × 2 (client + server)
    ↓ runtime
Cliente llama BookManager.findByPrimaryKey(42)
    → marshallBookThinRef(42) = [42]
    → Pyro4/HTTP → server
    → manager.findByPrimaryKey(42) → SELECT … → Book(…)
    → marshallBookGrossRef(book) = {"id":42, "title":"…", …}
    → cliente
    → unmarshallBookGrossRef({…}) = BookGrossRef(id=42, title="…")
    → UI lo modifica → ¿caché contaminado?

Flujo objetivo (nivel 3 con pydantic-rpc)

colofon/ipc/models/book.py  (pydantic_rpc.Message, frozen por defecto)
    ↓ sin generación — la clase ES el contrato
BookService en server envuelve el manager existente
    ↓ runtime
Cliente llama BookClient.findByPrimaryKey(42)
    → httpx POST /rpc/BookService/FindByPrimaryKey  {"id": 42}
    → pydantic-rpc deserializa BookId(id=42)
    → BookService.find_by_primary_key(BookId(id=42))
    → manager.findByPrimaryKey(42) → Book del dominio
    → Book.model_validate(b.__dict__) → Book(id=42, title="…")
    → pydantic-rpc serializa → JSON
    → cliente
    → Book.model_validate(r.json())  → Book(id=42, title="…", frozen)
    → UI trabaja con copia: book.model_copy(update={"title": "nuevo"})
    → caché almacena Book (frozen → contaminación imposible)

Referencias en el código

Concepto Archivo
Generador de marshalling fierrotools/objectTransfer/generateMarshallings.py
Runtime de marshalling fierrotools/objectTransferLib.py
Wrapper de marshalling colofon/util/marshallWrapper.py
Proxy Pyro / HTTP colofon/client/ipc/serverProxy.py
Caché cliente (fuerte) colofon/client/ipc/initManagers.pyCacheWrapper
Caché server (full) colofon/server/model/utils/cache.pyFullCacheDbObjectPool
Caché por request fierrotools/cache.py
Caché de unmarshalling fierrotools/objectTransferLib.pylru_unmarshallWithRefs
Lazy classes generadas colofon/client/ipc/lazyClasses.py (generado)
Contrato XML colofon/server/services.xml, colofon/client/ipc/modulesConfig.xml
Bug _invalidate remoto PR #3775 / Sentry FIERRO-SERVER-2N
Bug write-through caché PR #4166 (ticket 49032)
Fix código generado ruff PR #4062 (incompleto) → PR #4449
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment