El mismo código, con red o sin ella: el modo embebido de Zaris
La mayoría de los almacenes de datos distribuidos te hacen elegir, pronto y de forma permanente, entre dos mundos. Está el almacén "de verdad": un clúster que despliegas, al que apuntas un cliente por la red y con el que hablas a través de un protocolo de cable. Y está el almacén "falso" al que recurres en las pruebas unitarias: un diccionario en memoria, un mock, un stub hecho a mano que implementa lo justo de la interfaz para que la prueba compile. Los dos no comparten nada. Tu prueba ejercita el comportamiento del stub, no el del almacén, y el hueco entre ellos es exactamente donde se esconden los bugs.
Zaris colapsa esa elección. El mismo cliente tipado de .NET que habla con un clúster distribuido sobre TLS también ejecuta el almacén embebido dentro de tu proceso, sin socket, sin mapa de particiones y sin despliegue, y es el mismo motor de almacén, no un sustituto. Seleccionas cuál obtienes con una única cadena de conexión. Este artículo trata de cómo funciona eso, los casos para los que es genuinamente bueno y la línea dura donde el modo embebido se detiene y empieza un clúster.
Una cadena es todo el interruptor
Un cliente de Zaris siempre se configura a partir de una cadena de conexión. La cadena lleva el transporte, los endpoints semilla, el nombre del almacén y las opciones. Hay dos formas, y la diferencia entre ellas es la diferencia entre un clúster y un almacén embebido:
zaris://inproc/orders # embedded — store runs inside this process
zariss://zaris.mycorp.com:7863/orders # remote — connect to a cluster over TLS
El token de host inproc está reservado. Cuando el cliente lo ve, no hay endpoints que marcar, ni transporte que negociar, ni handshake de TLS, ni token: el almacén se sirve desde la memoria del proceso actual. Todo lo demás de tu código permanece idéntico:
// Embedded: no server to start, no port, no network.
IZarisClient store = await ZarisClient.ConnectAsync("zaris://inproc/orders");
// Later, in production — the ONLY line that changed:
IZarisClient store = await ZarisClient.ConnectAsync("zariss://zaris.mycorp.com:7863/orders?ca=/etc/zaris/ca.crt");
En ASP.NET Core es la misma historia a través de la inyección de dependencias: la cadena de conexión suele venir de la configuración, así que el cambio es un valor de configuración, no un cambio de código:
// Program.cs — one registration, driven entirely by the connection string.
builder.Services.AddClustronZaris("orders", builder.Configuration.GetConnectionString("orders")!);
Pon zaris://inproc/orders en appsettings.Development.json y zariss://.../orders en producción, y el mismo binario corre embebido en el portátil de un desarrollador y en clúster en la nube, sin nada recompilado.
Por qué "el mismo cliente" es el punto
La razón por la que esto importa está en el tipo que recibes de vuelta. ConnectAsync devuelve un IZarisClient —o, para toda la superficie de funcionalidades, IZaris— sin importar el modo. El modo embebido lo sirve una clase llamada ZarisInProcClient, y implementa exactamente las mismas interfaces que el cliente de clúster:
public interface IZaris : IZarisClient
{
ILeasesClient Leases { get; }
ILocksClient Locks { get; }
ICountersClient Counters { get; }
IScanClient Scan { get; }
IWatchClient Watch { get; }
IPubSubClient PubSub { get; }
IHashClient Hashes { get; }
IListClient Lists { get; }
ISetClient Sets { get; }
ISortedSetClient SortedSets { get; }
IStreamClient Streams { get; }
}
Cada una de esas superficies está presente en modo embebido, respaldada por el mismo motor de almacén que usa el clúster. Clave/valor Get/Put/Delete, bulk y batch, transacciones multielemento, TTL y expiración, compare-and-swap optimista, Watch para lecturas reactivas, Streams nativos con grupos de consumidores, las estructuras de datos replicadas: todo ello se resuelve contra el almacén embebido, no contra un mock.
Ese motor compartido es lo que hace del almacén embebido un objetivo de prueba fiel. Un bucle de compare-and-swap que depende de que un conflicto se reporte con precisión, una suscripción Watch que debe dispararse cuando una clave cambia, una transacción que debe hacer commit de forma atómica o no hacerlo en absoluto: cada uno se comporta embebido tal como se comporta en clúster, porque es literalmente el mismo camino de código evaluándolo. Un KvStatus.Conflict embebido significa lo que significa en producción. Un stub de diccionario escrito a mano no puede prometer eso.
Qué es realmente el host embebido
Vale la pena ser preciso sobre qué levanta zaris://inproc, porque los límites honestos caen directamente de ello. El host embebido es una topología fija de un único nodo que posee cada segmento. No hay mapa de particiones, porque solo hay un participante; no hay replicación, porque no hay una segunda copia; no hay transporte, porque no hay un segundo proceso al que llegar.
Lo que sí corre es la maquinaria real que querrías en una prueba. El almacén aprovisiona su segmento de antemano, conecta el servicio de leases, el servicio de watch y las métricas locales, y arranca el barredor de TTL a su cadencia normal, de modo que una clave a la que haces ExpireAsync con un TTL de 50 ms de verdad desaparece, con un temporizador, exactamente como lo haría en un clúster. La expiración, la contabilidad de desalojo, el versionado y las notificaciones de watch están todos activos. Es un almacén completo que resulta tener un tamaño de clúster de uno.
Dentro de un único proceso, los clientes se comparten por nombre de almacén. Dos llamadas ConnectAsync("zaris://inproc/orders") en el mismo proceso se resuelven al mismo almacén embebido, así que un valor que uno escribe, el otro lo lee, útil cuando tu prueba levanta un productor y un consumidor y espera que se vean mutuamente. Nombres de almacén distintos obtienen almacenes distintos; procesos distintos obtienen almacenes embebidos enteramente separados (más sobre ese límite abajo).
Dónde el modo embebido se gana su sitio
Pruebas unitarias e de integración que ejercitan el almacén real. Este es el uso estrella. Tu repositorio, tu capa de caché, tu coordinador de sagas: pruébalos contra zaris://inproc/test-{Guid}, obtén un almacén aislado y fresco por prueba, y ejercita la semántica genuina: versionado real para tus bucles CAS, expiración de TTL real con un temporizador real, atomicidad de transacción real, eventos de watch reales. Sin contenedor Docker que arrancar en CI, sin puerto que asignar, sin fragilidad de teardown. El almacén se crea en la primera conexión y muere con el proceso.
Aplicaciones de un solo proceso que pueden crecer más adelante. Una aplicación de escritorio, un trabajador en segundo plano, una herramienta de CLI o un servicio pequeño que necesita un almacén estructurado rápido hoy pero podría convertirse en un despliegue en clúster mañana. Empieza embebido. El día que necesites compartir esos datos entre instancias, cambias la cadena de conexión para que apunte a un clúster, y como programaste contra IZaris todo el tiempo, nada más se mueve.
Integraciones que se distribuyen embebidas por defecto. El proveedor de sesiones de Zaris para ASP.NET Core es un ejemplo concreto: conectar Zaris como tu almacén de sesiones es una llamada, y en su ejemplo corre enteramente embebido —
builder.Services.AddZarisSession(options =>
{
options.ConnectionString = "zaris://inproc/sessions";
});
— lo que significa que un desarrollador puede hacer F5 en la aplicación y tener sesiones funcionando con cero infraestructura, y luego moverse a zariss://.../sessions para un despliegue multiinstancia sin tocar el código de sesión.
Para usar el modo embebido, referencia el paquete Clustron.Zaris.InProc junto al cliente (tiene como objetivo .NET 8). El paquete se autorregistra: el cliente descubre y carga la factoría en proceso en el primer uso, así que simplemente referenciarlo es suficiente. En un ensamblado de pruebas donde prefieras ser explícito, new InProcBootstrap().Register(); lo conecta de forma determinista.
Dónde se detiene honestamente
El modo embebido es un almacén real, pero es un almacén de un único nodo, en memoria, y fingir lo contrario sería la pelusa de marketing que este blog intenta evitar. Tres límites duros, directos del diseño:
Sin durabilidad. El almacén embebido mantiene los datos en la memoria del proceso y en ningún otro sitio. No hay replicación ni log en disco en este modo: cuando el proceso termina, los datos se han ido. Eso es exactamente lo que quieres para una prueba (pizarra limpia en cada ejecución) y exactamente aquello en lo que no debes confiar para nada que necesites que sobreviva a un reinicio. El modo embebido no es una base de datos con la que respaldes tus datos de producción.
Sin alta disponibilidad, sin escalado hacia fuera. Un clúster de uno tolera la pérdida de cero nodos y escala a la memoria y la CPU de un único proceso. Todas las razones por las que recurrirías a Zaris distribuido —sobrevivir a un fallo de nodo sin pérdida de datos, dimensionar réplicas para durabilidad, escalar el rendimiento entre núcleos y máquinas— simplemente no aplican a un host embebido, porque solo hay uno de él. Cuando necesitas esas propiedades, necesitas el clúster.
Sin compartición entre procesos. El registro en proceso comparte un almacén por nombre dentro de un proceso. Dos procesos distintos que ambos abren zaris://inproc/orders obtienen dos almacenes independientes que no saben nada el uno del otro: no hay socket entre ellos, por definición. En el momento en que tus datos necesiten ser visibles para un segundo proceso o una segunda máquina, el modo embebido es la herramienta equivocada y un clúster es la correcta.
Ninguno de estos es un bug que haya que arreglar; son la definición de "embebido". El valor no es que el almacén en proceso sea secretamente distribuido, sino que el día que necesites distribución, lo único que cambias es la cadena que dice dónde vive el almacén.
La misma promesa, cumplida dos veces
La propuesta es pequeña y concreta. Escribes tu aplicación contra una única superficie de cliente. En una prueba, o en un portátil, o dentro de una herramienta de un solo proceso, esa superficie la sirve un almacén de Zaris real corriendo en tu propia memoria: rápido de crear, fiel en su semántica, y desaparecido cuando el proceso termina. En producción, el código idéntico habla con un clúster distribuido sobre TLS. El comportamiento que tus pruebas fijaron es el comportamiento que envías, porque nunca fue una implementación distinta, solo un lugar distinto para que el almacén viviera.
Esa es toda la idea detrás de zaris://inproc: el hueco entre "el almacén en mi prueba" y "el almacén en producción" es del ancho de una cadena de conexión, y tú decides, por entorno, en qué lado de él estás.