Ir al contenido

La API para aplicaciones

Latet protege lo que un agente envía a un modelo. Pero los programas también envían texto sensible a otros sitios: sus registros a Datadog, Splunk o Elastic, sus documentos al índice de una aplicación RAG. La API para aplicaciones permite a un programa pedir a un servidor de Latet, con el token de una clave de API, que:

  • analice un texto (scan): qué contiene de sensible, y dónde;
  • lo enmascare (mask): el mismo texto con valores ficticios realistas en lugar de sus valores;
  • lo restaure (unmask): los valores reales de nuevo en lugar de los ficticios;
  • olvide valores (forget): los quite de la caja fuerte de la clave, cuando una persona pide que sus datos desaparezcan (olvidar valores).

Es una función del servidor de Latet: las claves de API y sus tokens solo existen allí, y un equipo sin servidor no tiene nada de ella. Para probarla en tu propio equipo, arranca en él un servidor Community con el perfil de servidor de compose.yaml (operación del servidor).

Lo que aporta frente a una regla de ocultación:

  • El mismo valor ficticio cada vez. Un valor recibe el mismo valor ficticio en cada llamada, en cada instancia del servidor, mientras la caja fuerte de la clave lo conserve. En un servicio de registros, los gráficos y las búsquedas siguen funcionando, y se puede seguir a un cliente durante días de registros sin que nadie vea quién es. En un índice RAG, una persona sigue siendo la misma persona de un documento a otro.
  • Una sola caja fuerte para todo el flujo. Los documentos y las preguntas enmascarados con la misma clave comparten sus valores ficticios, así que un modelo razona sobre valores ficticios coherentes, y unmask vuelve a poner los valores reales en su respuesta.
  • Reversible, bajo control. Restaurar es un permiso propio, solo alcanza los valores ficticios de la clave, y cada restauración a través de la API para aplicaciones queda escrita en el registro de administración del servidor.

Dos páginas recorren los dos usos para los que se hizo: los registros y el RAG. La referencia HTTP da el contrato para programas en cualquier lenguaje, y la descripción OpenAPI de la API permite generar un cliente a partir de ella. Los programas en Python pueden usar el paquete latet-client, que funciona con LangChain, y los flujos de n8n los nodos de Latet para n8n.

Un programa envía una lista de textos y recibe una lista, en el mismo orden:

Ventana de terminal
printf 'Authorization: Bearer %s' "$(cat shop.key)" | curl -sS -H @- \
-H "Content-Type: application/json" \
-d '{"texts": ["Reembolsa el pedido de mario.rossi@acme.it a IT60X0542811101000000123456, por favor."]}' \
https://latet.acme.lan/_latet/apps/v1/mask
{"results": [{"text": "Reembolsa el pedido de tommaso.gallo@margie-6e5a.example a IT17F3945471087320356678487, por favor.",
"replacements": [{"type": "EMAIL", "class": "pii", "start": 23, "end": 56},
{"type": "IBAN", "class": "pii", "start": 59, "end": 86}],
"counts": {"EMAIL": 1, "IBAN": 1}}]}

shop.key contiene el primer token de la clave, el que escribe keys add (más abajo). printf, que la propia shell ejecuta, pasa la cabecera a curl por su entrada estándar (-H @-): así el token no aparece en la línea de órdenes de curl, donde otros usuarios del equipo podrían leerlo. Los valores ficticios son los de la clave: otra clave, u otro servidor, da otros. Con un token que puede restaurar, el mismo texto enviado a /_latet/apps/v1/unmask vuelve como era, con "restored": 2.

Una clave de API es un espacio en el servidor: sus reglas, su caja fuerte de valores ficticios, su historial. Puede tener varios tokens, los secretos que actúan en ese espacio, y cada token tiene sus propios permisos:

Permiso Lo que puede hacer el token
scan Decir qué contiene un texto y dónde; no cambia nada.
mask Sustituir los valores de un texto por los valores ficticios de la clave. Un token que puede enmascarar también puede analizar.
unmask Volver a poner los valores reales en lugar de los valores ficticios de la clave.
forget Quitar valores de la caja fuerte de la clave (olvidar valores). Ningún otro permiso lo incluye.

Da a cada programa solo lo que necesita. Una aplicación que envía sus registros necesita mask; la herramienta de soporte que lee una línea copiada del servicio de registros necesita unmask; una aplicación RAG que enmascara sus documentos y restaura la respuesta del modelo necesita los dos.

Ventana de terminal
latet server --server https://latet.acme.lan keys add "Shop" --out shop.key
latet server --server https://latet.acme.lan keys add-token "Shop" shipper --mask --out shipper.token
latet server --server https://latet.acme.lan keys add-token "Shop" support --unmask --out support.token
latet server --server https://latet.acme.lan keys tokens "Shop"

El primer token de una clave, el que crea keys add, tiene scan, mask y unmask, igual que el token de cada clave creada antes de que los tokens tuvieran permisos: esas claves funcionan como antes. El primer token de la clave tiene además una dirección para modelos, la puerta de enlace, y a través de ella sirve a los agentes de la clave: keys tokens lo señala con serves agents (has an address), para que no se revoque en lugar de un token añadido con los mismos permisos. Un token creado con keys add-token no tiene dirección, tenga los permisos que tenga, --mask --unmask incluidos: la puerta de enlace y el bridge del plugin lo rechazan (403), y solo llama a la API para aplicaciones.

Cada token se detiene por separado: keys tokens "Shop" --revoke t_4kxq2m7zap revoca uno por el id que muestra keys tokens, en pocos segundos en cada instancia. keys rotate "Shop" --token t_4kxq2m7zap --out shipper.token sustituye ese token por uno nuevo, con un secreto nuevo y un id nuevo, el mismo nombre y los mismos permisos; una clave con un solo token no necesita --token. Cada cambio queda en el registro de administración (token.added, token.revoked, key.rotated), con los ids de los tokens, nunca un secreto.

La API para aplicaciones solo acepta tokens de claves de API: el token del equipo de una persona se rechaza (403), igual que una solicitud que hace una página web. Las personas usan su panel.

scan y mask aceptan un kind para los textos de una llamada:

  • prose (el predeterminado): texto escrito por personas, como mensajes, tickets y documentos. Se enmascara como la puerta de enlace enmascara tus propios mensajes.
  • data: registros, fichas, la salida de un programa. Se enmascaran como la puerta de enlace enmascara la salida de una herramienta.

La diferencia está en lo que es probable: en el perfil balanced, los nombres solo se buscan en la prosa (con el modelo de nombres local instalado en el servidor), y una serie de dígitos solo cuenta como número de teléfono en la prosa, mientras que en un registro es mucho más a menudo un id. Con phone_regions: [IT] en las reglas de la clave, el número en Llámame mañana al 3471234567. recibe un valor ficticio como prosa y se queda como está como datos. Un documento JSON enviado como texto se enmascara como texto, conservando su formato, así que las posiciones de la respuesta son posiciones en él.

scan y mask usan los ajustes de la clave tal como están cuando llega la llamada: las reglas propias de la clave sobre los ajustes compartidos del servidor, como hace la puerta de enlace con las solicitudes de la clave. No hay reglas solo para la API. Se configuran con latet server config --key y latet server rules --key (ajustes compartidos y reglas de las claves de API).

  • El revisor local, cuando las reglas de la clave indican uno, lee cada texto de una llamada mask antes de la respuesta, diga lo que diga llm_review.inline para la puerta de enlace: una aplicación no tiene un momento posterior. Dispone de llm_review.budget_ms si está configurado, y si no de 30 segundos. Un texto cuya revisión falla sigue llm_review.on_failure: con stop (el predeterminado) la respuesta es blocked, con send es el texto tal como lo enmascararon las reglas. Así que, con un revisor caído, cada texto se bloquea hasta que vuelva a responder.
  • scan no ejecuta el revisor local. El revisor enseña a la caja fuerte lo que encuentra, y el análisis no cambia nada. En una clave con revisor, scan puede informar de menos de lo que mask sustituye.
  • El Revisor en directo nunca retiene el texto de una aplicación. Una aplicación no puede esperar a una persona: un texto se enmascara solo con las reglas y el revisor local, aunque las reglas de la clave activen el Revisor en directo.
  • La protección contra volcados se aplica a los textos data como a la salida de una herramienta: uno que contiene más valores personales de los que permite dump_guard recibe blocked.

Un texto bloqueado vuelve como {"blocked": "<motivo>"} sin texto, y el motivo no nombra ningún valor. Cuando algo funcionó de forma reducida en la llamada (un detector que no pudo leer un texto, una revisión que no terminó), la respuesta lo dice en "degraded", con los nombres de lo afectado, como ner o llm-review.

mask toma por real cada valor que encuentra, también los propios valores ficticios de la clave: nadie puede saber por una cadena si es un valor ficticio o un valor real que se le parece. Un texto enmascarado dos veces contiene valores ficticios de valores ficticios, y una restauración lo devuelve un paso atrás:

Paso Texto
Original Escribe a mario.rossi@acme.it
Enmascarado Escribe a tommaso.gallo@margie-6e5a.example
Enmascarado de nuevo Escribe a simone.desantis@humongous-0e54.example
Restaurado una vez Escribe a tommaso.gallo@margie-6e5a.example

Enmascara un texto una sola vez, a partir de su original. La puerta de enlace sigue la misma regla: un texto enmascarado con la API y luego enviado a un modelo a través de la puerta de enlace de Latet se vuelve a enmascarar allí, y la respuesta del modelo vuelve con los valores ficticios de la API. Por eso una aplicación RAG envía el texto enmascarado directamente al modelo y restaura la respuesta con la API (RAG).

Restaurar da los valores reales de los valores ficticios de la clave, sea cual sea la llamada o la conversación de la clave que los creó, también los del año pasado. No alcanza los valores ficticios de otra clave ni los de una persona: el texto que no es un valor ficticio de la clave se queda como está. El nombre de una persona solo vuelve entero: una parte suya sola (un nombre de pila) puede ser una palabra corriente, o el nombre de otra persona, y se queda como está. Cada restauración se escribe en el registro de administración antes de enviar su respuesta (apps.unmasked, con el token, cuántos textos y cuántos valores; nunca un texto ni un valor), y un registro que no puede escribirla rechaza la llamada (503). El primer token de la clave, que puede analizar, enmascarar y restaurar, también sirve a agentes a través de la puerta de enlace: sus respuestas se restauran como las de cualquier agente, sin una entrada del registro de administración por cada restauración. Un token creado con keys add-token nunca lo hace, así que cada una de sus restauraciones es una entrada apps.unmasked. Da unmask a las herramientas de las personas que pueden ver los datos reales, y lee el registro con latet server log --action apps.unmasked.

Enmascarar también revela algo. Con un token que puede enmascarar y acceso a los registros enmascarados, alguien puede confirmar una sospecha: enmascarar mario.rossi@acme.it y buscar su valor ficticio en los registros. Si está, Mario Rossi es cliente. Por eso da mask solo a los programas que lo necesitan, guarda el token donde guardan sus secretos, y trata los registros enmascarados como datos seudonimizados, no anónimos.

Analizar también revela algo. No crea valores ficticios ni cambia nada, pero informa de los valores que la caja fuerte de la clave ya conoce, dondequiera que aparezcan. Un token que solo puede analizar puede, por tanto, confirmar que un valor que ningún detector encontraría por sí solo (un nombre en un texto data, un valor que una regla de la clave encontró antes de que la quitaran) es uno de los de la clave. Da scan con el mismo cuidado que mask.

Olvidar también revela algo. Su respuesta dice cuántos de los valores guardaba la caja fuerte de la clave, así que un token que puede olvidar puede confirmar que un valor es de la clave, como uno que puede analizar. Da forget solo al proceso que atiende las peticiones de borrado; cada olvido queda en el registro de administración (apps.forgotten, con el token y las cifras, nunca un valor). Ninguno de los tokens que crea keys add puede olvidar: forget se da a un token propio (keys add-token "Shop" erasure --forget).

La caja fuerte de una clave conserva cada valor que ha enmascarado, con su valor ficticio, mientras la clave exista: una restauración funciona con una línea de registro del año pasado, y un cliente conserva el mismo valor ficticio durante años. Los valores no caducan: un valor sale de la caja fuerte cuando se olvida (olvidar valores), y todos se van con la clave, ya que latet server keys remove borra su espacio, y con él su caja fuerte. Un valor encontrado una vez se enmascara desde entonces dondequiera que aparezca, incluso donde ningún detector lo volvería a encontrar: la dirección de un cliente que una regla de la clave encontró una vez, y que luego se quitó, sigue enmascarada.

Un espacio de registros puede contener millones de valores, así que una instancia no guarda en memoria la caja fuerte de una clave. Guarda los valores que la clave usó por última vez (key_working_values, 5.000 por defecto, unos 10 MiB por clave) y las huellas de toda la caja fuerte: una huella de 64 bits de cada palabra de sus valores y de sus valores ficticios, calculada con una clave secreta, unos 15 MB por un millón de valores. Las palabras de una llamada se buscan primero entre las huellas: una palabra sin huella no es de ningún valor ni de ningún valor ficticio, y a PostgreSQL no se le pregunta por ella. Los valores y los valores ficticios cuyas palabras tienen una se leen de PostgreSQL, todos los de la llamada de una vez, y cada uno se descifra y se compara con el texto antes de enmascarar o restaurar nada, porque dos palabras pueden tener la misma huella. Ni las huellas ni el índice de las palabras en PostgreSQL contienen una palabra en claro. Antes de cada llamada la instancia lee en PostgreSQL el contador de cambios de la clave y recoge lo que las otras instancias guardaron u olvidaron desde entonces, así que un valor guardado en una instancia antes de que empiece una llamada lo encuentra esa llamada en cualquier otra, aunque el mensaje que lo anunciaba se haya perdido. Una clave con pocos valores funciona igual: solo la caja fuerte de una persona de 35.000 valores o menos se guarda entera en memoria (operación del servidor).

Secretos pegados a otras letras. En PostgreSQL, un secreto escrito como una sola serie de letras y dígitos, sin espacios ni puntuación, se encuentra por su palabra, y por sus seis primeros y seis últimos caracteres cuando otras letras o dígitos están pegados a él: por un lado, en el borde de la serie del texto que lo contiene, y por ambos lados (xhunter2y), dentro de esa serie, que una llamada lee de seis en seis caracteres. Una llamada lee como mucho un millón de esas posiciones: por encima, sus series siguientes se leen solo por sus palabras y sus bordes. Un secreto de seis caracteres o menos, o cuya palabra en un borde pegado tiene seis caracteres o menos, no lo encuentra ninguna palabra del texto cuando está pegado. Por eso una instancia guarda en memoria para siempre los secretos cortos de una clave, los de una sola serie de 16 caracteres o menos o con una palabra así de corta en un borde: los key_held_secrets más recientes (1.000 si los ajustes del servidor no dicen otra cosa), que se encuentran estén pegados como estén, también por encima del millón de posiciones de una llamada. Uno más antiguo, o uno más largo, se encuentra donde nada está pegado a él, pegado por un lado cuando la palabra de ese borde tiene siete caracteres o más, y pegado por ambos lados cuando es una sola serie de siete caracteres o más, dentro del millón de posiciones de la llamada. Sube key_held_secrets para una clave que ve muchos secretos cortos, como un token de sesión en cada línea de registro: cada uno que se guarda ocupa unos 23 KB en cada instancia que sirve a la clave, la mayor parte en el detector que lo encuentra, y cada texto que la clave enmascara o restaura se recorre para buscarlo, lo que hace sus llamadas un poco más lentas. Medida con Latet 0.10.0 en el equipo de velocidad, con un token de sesión propio en cada línea (un valor más que guardar en cada una), una clave nueva con 1.000 de esos secretos guardados enmascaró 73 líneas por segundo y restauró 1.935, frente a 75 y 2.103 sin ninguno guardado. Un valor ficticio se encuentra pegado como su secreto: conserva los signos del secreto en su sitio, así que un valor ficticio pegado de modo que ninguna de sus palabras quede suelta es el de un secreto guardado en memoria, y se encuentra mientras el secreto se guarda. Como en todo Latet, un valor se vuelve a reconocer a partir de cuatro caracteres, el nombre de una persona a partir de tres (valores conocidos).

Secretos cortos. Un secreto de menos de doce caracteres (password=1234, un token corto) recibe un valor ficticio más largo de los mismos tipos, con sus signos en su sitio: quince dígitos para un número (con su signo y su punto decimal, para que un número JSON siga siendo un número), doce dígitos hexadecimales para un valor hexadecimal, si no doce letras y dígitos. Un valor ficticio tan corto como su valor aparecería por casualidad en los identificadores y los números de otras líneas de registro, y una restauración cambiaría líneas que nunca contuvieron el secreto.

Un valor ficticio corto que la caja fuerte de una clave dio antes de Latet 0.10.2 (de ocho caracteres o menos) se queda, como todo valor ficticio. unmask, que lee cada valor ficticio de la caja fuerte de la clave, solo lo sustituye por el valor donde está como palabra completa, sin letra, dígito ni guion bajo al lado. El coste: donde el valor de un secreto así se enmascaró dentro de una palabra o un número más largo (postgres en postgresql://, 1234 en order_id=9912343), unmask deja ahí su valor ficticio. Una conversación del primer token de la clave, a través del gateway o del plugin, lo restaura como antes, también dentro de una palabra, porque solo restaura los valores ficticios que se le mostraron. Mientras una instancia del servidor siga ejecutando la 0.10.0, da valores ficticios cortos a los nuevos secretos cortos, y también esos se quedan: actualiza cada instancia.

Qué conserva una llamada que falla. Una llamada mask que falla mientras enmascara (un 507, una confirmación que no llega a PostgreSQL) no conserva ninguno de sus valores nuevos cuando ocupaba para ellos una de las transacciones de la instancia, tres a la vez con el database_pool predeterminado: el espacio de la clave los olvida. Si esas estaban ocupadas, la llamada conservó cada valor nuevo sobre la marcha, y esos se quedan en la caja fuerte de la clave. Un fallo después del enmascaramiento, en la revisión local o al preparar la respuesta, llega después de la confirmación, así que los valores nuevos de la llamada también se quedan. Volver a enmascarar los textos es seguro en cualquier caso: un valor que la caja fuerte conservó tiene su valor ficticio, y uno que olvidó recibe otra vez uno. Un agente de la misma clave que usó mientras tanto un valor olvidado a través de la puerta de enlace ya envió su valor ficticio, que ninguna restauración posterior devolverá.

  • La caja fuerte. La caja fuerte de una clave admite los vault_limit valores del servidor (200.000 si los ajustes del servidor no dicen otra cosa), o un límite propio: latet server keys limit "Shop" 5000000 (de 1.000 a 100.000.000; default le devuelve el del servidor). Una caja fuerte llena rechaza con 507 la llamada mask que añadiría un valor: no vuelve nada de la llamada, y nada sale en claro. El estado del servidor lista las identidades al 90 % de su límite, y la métrica latet_vault_near_limit las cuenta, así que sube un límite antes de alcanzarlo. Cada instancia cuenta los valores nuevos de una llamada antes de confirmarlos: dos instancias, o una llamada y un agente de la misma clave, pueden superar el límite en los valores nuevos de una llamada cada uno antes de que se rechace el siguiente valor. Un índice de las huellas de una caja fuerte se construye hasta 16.777.216 huellas, unos ocho millones de valores del tipo que mide velocidad: por encima, no se construye ningún índice mientras dura el espacio de la clave, y sus llamadas buscan sus palabras solo en PostgreSQL, más despacio.
  • Una llamada admite como mucho 1.000 textos y el apps_request_mb del servidor (8 MiB si sus ajustes no dicen otra cosa, nunca más que su max_request_mb): una llamada espera en memoria al hilo de la clave, y latet-client divide una lista más larga en llamadas de 8 MiB como mucho. Los textos cuyas palabras nombran más de 100.000 valores o valores ficticios de la caja fuerte se rechazan enteros con 422, con palabras fijas que no nombran ningún valor: no se enmascara ni se restaura nada de la llamada, así que envía menos textos por llamada. Un 503 es otra cosa: un almacén del servidor (PostgreSQL, Valkey) no respondió, y la misma llamada puede volver a enviarse más tarde (referencia HTTP).
  • Llamadas simultáneas. Una clave tiene como mucho requests_in_flight solicitudes de aplicaciones en una instancia (16 si los ajustes no dicen otra cosa); por encima, 429 con Retry-After.

Las llamadas de una clave se ejecutan una tras otra en una instancia, en el hilo de su espacio; las llamadas de claves distintas, y las que llegan a instancias distintas, se ejecutan a la vez. Para ir más rápido con una clave, pon más instancias detrás del equilibrador de carga, o varios workers en una máquina Linux (varias instancias en una máquina).

Medido con Latet 0.11 en una instancia (Intel i9-10850K, 64 GB, Windows 10, PostgreSQL 18 en Docker Desktop en el mismo equipo, los ajustes por defecto del servidor): 40.000 líneas de registro de unos 200 caracteres, cada una con una dirección de correo y una dirección IPv4 pública, la mitad de las cuales nombra a un cliente ya visto, en llamadas de 500 como data; después 50 documentos de unos 20 KB de prosa con diez valores cada uno, uno por llamada. Las líneas por segundo son las líneas divididas por el tiempo que tardaron sus llamadas; el tiempo de un documento es la mediana de sus 50 llamadas:

La caja fuerte de la clave Enmascarar, líneas por segundo Restaurar, líneas por segundo Un documento, enmascarar / restaurar
Nueva, aprende sus valores sobre la marcha 191 2.894 126 / 31 ms
200.000 valores 871 2.960 117 / 32 ms
1.000.000 de valores 773 2.967 124 / 32 ms

Cada documento trae valores nuevos para la clave, que los escribe en PostgreSQL mientras los enmascara: los mismos 50 documentos enmascarados de nuevo, con sus valores ya conocidos, tardaron 16,6 ms con 200.000 valores y 16,4 ms con 1.000.000 (restaurar, 23 ms en ambos casos). Una clave nueva también escribe la mayoría de los valores de sus líneas, y por eso enmascara menos.

Latet 0.10.0, medido de la misma manera en el mismo equipo, enmascaraba 350 y 304 líneas por segundo con 200.000 y 1.000.000 de valores y 153 para una clave nueva, restauraba 2.114 y 2.014, y enmascaraba un documento en entre 163 y 182 ms. Latet 0.11 se proponía enmascarar al menos 1.000 líneas como estas por segundo en una instancia con 200.000 y con 1.000.000 de valores, y un documento en 30 ms o menos: enmascara 871 y 773, y un documento en 30 ms solo cuando la clave ya conoce sus valores. La mayor parte del tiempo de una llamada de enmascarar se va en los detectores, y una parte en esperar a PostgreSQL.

Una instancia encuentra los valores de una clave mediante las huellas de las palabras de su caja fuerte (la caja fuerte de una clave), que ocupan unos 15 MB por un millón de valores (14,77 MB con 1.000.000) y se construyen en segundo plano mientras se abre el espacio de la clave: en 1,5 segundos con 1.000.000 de valores (la mediana de tres aperturas, de 1,42 a 1,62 s). Hasta que están listas, las llamadas de la clave buscan sus palabras solo en PostgreSQL: en una medición anterior con Latet 0.11, con las huellas desactivadas, la clave de 200.000 valores enmascaró 494 líneas por segundo y restauró 1.382. Cada llamada lee primero el contador de cambios de la clave, 0,51 ms de mediana, y recoge lo que cambió desde entonces: 1,87 ms por un valor nuevo y 2,11 ms por 100, también de mediana.

En una llamada de textos enmascarados antes, solo los nuevos se vuelven a leer, pero cada uno de los demás se sigue buscando y comparando con lo que la clave enmascaró: 500 textos enmascarados antes y uno nuevo tardaron 168 ms, el nuevo solo 6,1 ms (medianas de tres ejecuciones).

En una máquina Linux, los workers se suman. Medido en Docker en el mismo equipo (una máquina virtual Linux de 20 procesadores y 16 GB, con PostgreSQL 18.6 y Valkey 9.1.2 en contenedores al lado), con latet server run --workers y una clave de API de 200.000 valores: 200.000 líneas distintas del tipo descrito arriba en llamadas de 62, cuatro conexiones por worker, cada número de workers durante 20 segundos, la mediana de tres rondas; la memoria es la memoria residente de cada worker tras las rondas:

Workers Enmascarar, líneas por segundo Restaurar, líneas por segundo Memoria por worker
1 1.190 1.947 227 MB
2 2.260 3.711 227 MB
4 4.253 6.594 216 MB
8 7.477 10.147 185 MB

Ocho workers enmascararon 6,28 veces lo que uno y restauraron 5,21 veces, donde Latet 0.11 se proponía llegar al menos a 5 veces en ambos; ninguna ronda con ocho workers restauró a menos de 5,07 veces. El kernel reparte las conexiones entre los workers según un hash, no por turnos, así que unos workers reciben más que otros: de 1 a 10 de las 32 en una ronda con ocho workers. El proceso que arranca los workers ocupaba de 47 a 48 MB a su lado. Estas llamadas llegan desde varias conexiones a la vez, así que compara las filas de esta tabla entre sí, no con la primera tabla.

Escribir valores nuevos necesita conexiones a PostgreSQL: una instancia, y cada worker, mantiene como mucho database_pool (10 si los ajustes del servidor no dicen otra cosa) para el trabajo de todas las identidades, y deja que tres décimas partes de ellas (tres de diez) las ocupen llamadas que conservan sus valores nuevos en una transacción cada una; las demás conservan cada valor según llega, más despacio. Sube database_pool para una instancia que enmascara para muchas claves nuevas a la vez, y con él el max_connections de PostgreSQL (operación del servidor). scripts/bench_apps.py, en el repositorio, mide un servidor tuyo como se midió la primera tabla.

Latet 0.11 cambia las tablas. Detén cada instancia y cada worker, en cada máquina, ejecuta latet server migrate una vez con la 0.11 y luego arráncalos todos con ella (actualizar). Una instancia no arranca sobre tablas de otra versión, así que entre la parada y el arranque no se responde ninguna llamada, y latet server migrate no cambia nada mientras una instancia está viva, una de la 0.10 durante 30 segundos después de detenerse. La migración añade a cada fila de palabra de cada caja fuerte un sitio vacío para su huella y para el número del cambio que la escribió por última vez, con sus índices: en una base de datos con cajas fuertes grandes tarda más que las migraciones anteriores, y las huellas mismas se calculan después, en segundo plano.

Una caja fuerte guardada por Latet 0.10 aún no tiene huellas. Tras el reinicio, la primera instancia que construye el espacio de una clave responde enseguida a las llamadas de la clave, buscando sus palabras en PostgreSQL como hacía la 0.10, y calcula las huellas de la caja fuerte en segundo plano, una vez, una instancia cada vez; después cada instancia construye su propio índice de esas huellas y lo usa. Hasta entonces las llamadas de la clave enmascaran los mismos valores, más despacio (velocidad: 494 líneas por segundo con 200.000 valores y las huellas desactivadas, en una medición anterior), y ninguna se rechaza por ello. Mientras una instancia calcula las huellas de una caja fuerte, las demás claves y personas cuyo espacio abre entretanto pueden servirse también con la búsqueda en PostgreSQL, hasta que su índice esté construido. Cuando las huellas aún no están todas calculadas o no puede construirse un índice (PostgreSQL o Valkey que no responden, una actualización del índice que falló), las llamadas siguen con la búsqueda en PostgreSQL hasta que el índice se construye de nuevo: enseguida cuando otra instancia ha calculado las huellas, y si no 10, 20 y 30 segundos después de cada intento y luego una vez por minuto. El registro dice una vez cuando un índice deja de usarse y una vez cuando vuelve a usarse. Una caja fuerte por encima del tamaño para el que se construye un índice (límites) sigue con la búsqueda en PostgreSQL mientras dura su espacio. Las palabras mismas quedan como las hizo la 0.10: la 0.11 no las vuelve a hacer.

Una versión que cambia cómo se hacen las palabras de una caja fuerte (el registro de cambios lo dice) vuelve a hacer las palabras de una clave en la primera instancia que construye su espacio, antes de que ese espacio responda a la primera llamada de la clave: Latet 0.10.0 tardaba unos 40 segundos para 200.000 valores y cuatro minutos y medio para 1.000.000 en el equipo indicado arriba. Una llamada que llega mientras tanto espera, y una instancia construye cuatro espacios a la vez, así que unas pocas claves grandes retrasan las primeras llamadas de las demás claves en ella: tras una versión así, envía a cada clave grande una llamada, con un tiempo de espera suficiente, antes de que sus programas vuelvan a arrancar.

Mientras instancias de dos versiones se ejecutan a la vez, lo que permite una versión que no cambia las tablas, un valor que escribe una instancia más antigua lo encuentran las más nuevas mientras lo tienen en memoria, pero no en PostgreSQL hasta que el espacio de la clave se vuelve a construir. Cuando la API para aplicaciones está en uso, detén cada instancia más antigua antes de que arranquen las nuevas, aunque las tablas no cambien.