Fallar cerrado, nunca destructivo: por dentro del ciclo de vida de licencias de Zaris
El licenciamiento es la parte de un producto en la que la mayoría de los ingenieros nunca quieren pensar, y se nota. El patrón habitual es una bomba de tiempo: se comprueba una fecha, y cuando pasa, el software te castiga — lanza una excepción al arrancar, corrompe o borra el estado local para evitar que lo "robes", o simplemente revienta y se lleva tu data con él. Ese diseño trata a tus propios operadores como adversarios, y falla justo en el momento en que menos te lo puedes permitir: en producción, después de que el reloj girara silenciosamente.
Zaris adopta la postura opuesta. La comprobación de licencia es el interruptor más crítico para la seguridad de un almacén de datos distribuido, así que está construida como tal: una única función de política pura y determinista, un camino de aplicación elegante y totalmente reversible que nunca destruye un byte, y un reloj por máquina que se reanuda en el instante en que activas una clave. Este post recorre cómo funciona realmente ese ciclo de vida en el código que se envía — porque la ingeniería interesante aquí está en lo que se niega a hacerte.
El ciclo de vida, como máquina de estados
Una máquina nunca licenciada avanza por un conjunto pequeño y explícito de etapas. Una prueba automática de 30 días comienza en la primera ejecución sin clave y sin registro. Cuando transcurre, el producto no se detiene — entra en una ventana de gracia donde la consola web queda bloqueada pero el plano de datos sigue sirviendo. Solo después de que la ventana de gracia también transcurra, el producto deja de servir elegantemente, con tu data preservada. Activar una licencia válida en cualquier punto cortocircuita directo a Licensed.
Los dos defaults son ambos de 30 días — una prueba base de 30 días, luego una gracia de producto de 30 días tras su fin — y ambos son constantes simples, sobrescribibles por despliegue:
public const int DefaultBaseTrialDays = 30;
public const int DefaultProductGraceDays = 30;
Hay dos estados más, casi terminales, en los que una clave puede ponerte directamente: Licensed (una licencia Enterprise válida, todo funciona, el reloj local se ignora) y Non-Production (una licencia válida de nivel dev, todo funciona pero marcado como no apto para producción).
La política es una función pura
La tentación en el código de licenciamiento es esparcir comprobaciones if (DateTime.Now > expiry) por la consola, el servicio de gestión y el host del nodo — y luego verlas discrepar, porque una leyó el reloj un segundo después, otra cacheó un archivo obsoleto, otra olvidó la ventana de gracia. Zaris tiene exactamente un lugar que decide, y es una función pura sin I/O, sin lecturas de reloj, y sin llamadas al SDK:
public static LicenseVerdict Evaluate(
DateTime nowUtc,
PaidLicenseStatus paid, // resolved Keyright license (or None)
TrialStatus trial, // the local machine-bound trial marker
int baseTrialDays = DefaultBaseTrialDays,
int productGraceDays = DefaultProductGraceDays)
Todo de lo que depende el veredicto es un argumento. La consola web, el servicio de gestión y cada host de nodo resuelven sus propias entradas — la hora actual, el estado de la licencia de pago, el marcador de prueba local — y las pasan a través de esta misma función, lo que garantiza un veredicto idéntico en todas partes. Y como la lógica de transición no toca nada externo, cada arista de la máquina de estados es exhaustivamente comprobable por tests unitarios sin mockear un sistema de archivos o un reloj: pasas un nowUtc y aseveras la etapa.
La salida es un único registro que lleva tanto el mensaje de cara al humano como los dos booleanos que impulsan la aplicación:
public sealed record LicenseVerdict(
LicenseStage Stage,
bool IsConsoleGated, // -> the console returns HTTP 402
bool IsProductStopped, // -> the data plane refuses to serve
bool IsPaid, bool IsTrial,
int DaysRemaining, DateTime? ExpiryUtc,
string StatusBadge, string Message);
Fallar cerrado — pero cerrado significa la prueba, no un crash
Cuando el resolutor de licencia de pago no puede alcanzar el servicio emisor, o un archivo de lease es ilegible, o cualquier cosa sale mal al resolver la licencia de Keyright, el servicio no lanza una excepción y no asume optimistamente que estás licenciado. Recurre al reloj de prueba local:
try { paid = _paid.Resolve(); }
catch { paid = PaidLicenseStatus.None; } // fail-closed to the trial clock
"Fallar cerrado" aquí deliberadamente no es "fallar detenido". Un parpadeo transitorio de red durante la resolución de licencia te deja sobre el propio marcador de prueba offline de la máquina, que es autoritativo y no necesita red. Si todavía estás dentro de tu ventana de prueba o gracia, nada cambia para tu clúster en marcha. Solo el genuino paso del tiempo — no una caída — te avanza hacia una detención.
Dos puntos de aplicación, ninguno destructivo
El veredicto expone dos interruptores independientes porque la consola y el plano de datos se condicionan en momentos distintos, a propósito.
La puerta de la consola (IsConsoleGated). En el momento en que la prueba transcurre, las rutas de admin y de la API /v1 de la consola web devuelven 402 Payment Required con un mensaje que apunta a cómo reactivarla. Esto es middleware que se sienta frente a la consola — no toca el plano de datos en absoluto:
if (sw.IsGated) {
context.Response.StatusCode = StatusCodes.Status402PaymentRequired;
// ... reason + support URL ...
}
Crucialmente, durante esta etapa el producto sigue corriendo. Tus clientes siguen leyendo y escribiendo; la replicación, la coordinación y el plano de datos quedan intactos. Lo que pierdes es la superficie de gestión, con advertencias sonoras que te dicen que la ventana se ha cerrado — un empujón seguro para producción, no una caída.
La puerta del plano de datos (IsProductStopped). Solo después de que la ventana de gracia transcurra el servidor empieza a rechazar operaciones de datos. Lo hace devolviendo un estado de error — nunca borrando data, nunca reventando:
if (_license?.IsProductStopped == true && !IsLicenseExempt(request.Command))
return new ZarisResponse {
Success = false,
Status = KvStatus.Unavailable,
Error = _license.ProductStoppedMessage // "...activate a license to resume"
};
Dos detalles hacen esto humano en lugar de hostil. Primero, el motor central no toma ninguna dependencia de la implementación de licenciamiento en absoluto — consulta una diminuta interfaz ILicenseGate con dos miembros (IsProductStopped, ProductStoppedMessage) que vive en el ensamblado de abstracciones. El host del nodo cablea el servicio real respaldado por Keyright por detrás de ella. El almacén de datos no sabe qué es Keyright.
Segundo, un servidor detenido no es un servidor muerto. Un conjunto específico de comandos permanece exento para que un operador siempre pueda conectarse, inspeccionar y recuperar:
private static bool IsLicenseExempt(ZarisCommandType cmd) =>
cmd == ZarisCommandType.Hello
|| cmd == ZarisCommandType.ClusterInfoRequest
|| /* the read-only diagnostic snapshots */ ;
El handshake, la sonda de cluster-info y los snapshots de diagnóstico de solo lectura siguen funcionando todos. Puedes conectarte, ver el estado del clúster, confirmar que la data está intacta, y activar una licencia — todo mientras está "detenido". El estado se preserva en disco/en memoria exactamente como estaba; lo único que se retiene es servir lecturas y escrituras, y eso se retiene de forma reversible.
Un reloj por máquina
Una prueba que se reinicia cada vez que reinstalas, o que concede unos 30 días frescos por nodo en un clúster, no es una prueba. Zaris hace cumplir una máquina = un asiento: un único reloj de prueba compartido por cada proceso de Zaris en una caja — manager, nodos co-ubicados, consola — independientemente de la cuenta de servicio bajo la que corran. Eso se logra poniendo el marcador de prueba en una ubicación de ámbito de máquina (CommonApplicationData / ProgramData) y espejándolo a una segunda copia de ámbito de máquina.
El marcador en sí está construido para resistir la manipulación casual mientras es honesto sobre que no es DRM:
- Está ligado a una huella de máquina — un hash del GUID de máquina del SO (
MachineGuiden Windows,/etc/machine-iden Linux) más el nombre de la máquina. Un archivo de marcador copiado a una máquina diferente simplemente se ignora allí; no le concede nada a esa máquina. - Cada copia está firmada con HMAC. Una copia editada o falsificada se detecta y se trata como manipulada.
- El inicio válido más temprano gana entre las copias espejadas, y las extensiones solo se mueven hacia adelante, de modo que una prueba no puede reiniciarse ni rebobinarse silenciosamente borrando uno de dos archivos.
- Se rastrea un
lastSeenmonótono; si el reloj del sistema se retrocede más allá de una tolerancia de 24 horas, eso también se marca como manipulación — no puedes comprar tiempo retrasando el reloj.
Y un marcador manipulado nunca se recompensa. En lugar de conceder silenciosamente o reiniciar silenciosamente, la política trata una manipulación detectada como el peor caso y pasa directo a detenido:
if (trial.Tampered)
return Stopped(tampered: true); // never reward an altered/rolled-back marker
La limitación honesta, documentada justo en el código fuente: borrar todas las copias del marcador sí reinicia la prueba. Esa es una frontera aceptada del modelo de amenazas — el objetivo es elevar el listón contra la elusión casual en una base de código visible como open source, no fingir que esto es protección anticopia inquebrantable.
Las extensiones extienden; nunca reinician
¿Necesitas más tiempo de evaluación? Una clave de nivel Trial emitida por el vendedor extiende la ventana — por ejemplo a 90 días en total — en lugar de iniciar una segunda prueba. Cuando se observa tal clave, su caducidad se persiste en el marcador, moviendo el fin de la prueba hacia adelante:
// only ever forward — never shortens, never resets the base start
var extended = current is { } c && c >= untilUtc ? c : untilUtc;
Persistir la extensión importa: significa que la ventana más larga (y su gracia) sobrevive a que la clave caduque después o a que la máquina se desconecte. No necesitas permanecer conectado al servicio de licencias para conservar la extensión que te concedieron.
La activación se reanuda sin reinicio
Como un producto detenido es reversible, la activación tiene que surtir efecto sobre un proceso vivo. Y lo hace. El servicio de licencia cachea su veredicto y lo reevalúa en un refresco periódico (cada cinco minutos por defecto), y una activación explícita fuerza una reevaluación inmediata:
public async Task<LicenseVerdict> ActivateAsync(string licenseKey, ...)
{
await _paid.ActivateAsync(licenseKey.Trim(), ct);
return Refresh(); // new verdict is live at once; the data plane resumes on its own
}
Un clúster detenido que licencias en medio de una caída simplemente empieza a servir de nuevo en la siguiente evaluación — sin reinicio rodante, sin recarga de data, sin ventana donde el estado esté en riesgo.
Hay tres formas de meter una clave, acordes a cómo la gente realmente ejecuta las cosas:
Set-ZrLicense -Key "LIC-…"desde el admin shell aplica la clave en cada máquina manager del workspace (cada una vincula su propio asiento por máquina).-Deactivatelibera un asiento.Get-ZrLicenseStatusimprime la etapa y el badge de cada manager.ZARIS_SUPPORT_KEYcomo variable de entorno se activa una vez al arranque — fíjala, reinicia, listo. Es idempotente: reactivar una máquina ya vinculada no consume ningún asiento extra.- Un token offline para despliegues air-gapped: un blob firmado preemitido que Keyright verifica enteramente offline, sin llamar a casa.
Un puñado de variables de entorno afina el resto para un despliegue dado: ZARIS_SUPPORT_WINDOW_DAYS y ZARIS_PRODUCT_GRACE_DAYS ajustan las dos ventanas, ZARIS_DATA_ROOT reubica el marcador, y ZARIS_LICENSE_SERVICE_URL apunta a un servicio emisor autohospedado.
Por qué construirlo así
Habría sido menos código lanzar una excepción en una fecha caducada. La maquinaria extra — una función de política pura, una puerta de dos etapas, comandos de recuperación exentos, un marcador espejado a prueba de manipulación, reanudar sin reinicio — toda existe para honrar un principio: una decisión de licenciamiento nunca debe costarte tu data, y siempre debe ser reversible haciendo lo obvio.
La prueba transcurre y sigues corriendo, con advertencias. La gracia transcurre y el almacén deja de servir mientras preserva todo y permanece alcanzable para la recuperación. Activas una clave y se reanuda, en vivo. Y toda la decisión viene de una única función determinista con la que cada componente está de acuerdo, así que no hay ningún rincón del sistema que haga cumplir silenciosamente algo diferente. El licenciamiento es donde mucho buen software se vuelve contra sus propios usuarios; el objetivo aquí fue hacerlo el único lugar donde un almacén distribuido tiene permiso de decir "no" sin tener jamás permiso de decir "se fue".