Saltar al contenido principal

Escribir un generador de servicio

El generador http deja que un servicio escrito por usted decida los valores. Esta página es la otra mitad: cómo escribir ese servicio, en Node, Python o Java, de modo que sea rápido, correcto y — la parte que todos fallan — reproducible.

Abajo hay un servicio funcionando por lenguaje. Cada uno cubre los dos modos: inventa números de cuenta cuando le piden valores, y agrega un dígito de control de Luhn cuando le entregan valores para procesar.

Qué debe hacer el servicio

El contrato completo está en la página del generador. Para escribir uno bastan cuatro líneas de él:

  • llega un POST con X-TDC-Count: N y X-TDC-Seed: <hex>;
  • el cuerpo son N valores, uno por línea — o está vacío, lo que significa «invéntelos»;
  • responda con exactamente N líneas, en el mismo orden;
  • texto plano, no hace falta JSON en ninguna parte.

El servicio, en cinco lenguajes

import { createServer } from 'node:http';

/** FNV-1a (32-bit). The same three lines in every language — that is the point. */
function fnv1a(text) {
let h = 0x811c9dc5;
for (const ch of Buffer.from(text, 'utf8')) {
h ^= ch;
h = Math.imul(h, 0x01000193) >>> 0; // Math.imul keeps it 32-bit
}
return h >>> 0;
}

/** Source mode: an 8-digit account for row `i`, decided only by (seed, i). */
function accountFor(seed, i) {
return String(fnv1a(`${seed}#${i}`) % 100000000).padStart(8, '0');
}

/** Handler mode: the Luhn check digit of what was sent. */
function luhn(number) {
let sum = 0;
let dbl = true;
for (let i = number.length - 1; i >= 0; i--) {
let d = number.charCodeAt(i) - 48;
if (d < 0 || d > 9) continue;
if (dbl) {
d *= 2;
if (d > 9) d -= 9;
}
sum += d;
dbl = !dbl;
}
return String((10 - (sum % 10)) % 10);
}

createServer((req, res) => {
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const count = Number(req.headers['x-tdc-count'] ?? '0');
const seed = String(req.headers['x-tdc-seed'] ?? '');
const body = Buffer.concat(chunks).toString('utf8');

const out =
body.length === 0
? Array.from({ length: count }, (_, i) => accountFor(seed, i)) // source
: body.split('\n').map((line) => line + luhn(line)); // handler

const payload = out.join('\n');
res.writeHead(200, {
'Content-Type': 'text/plain',
'Content-Length': Buffer.byteLength(payload),
});
res.end(payload);
});
}).listen(5701, '127.0.0.1');

Ejecútelo con node service.mjs y apunte src a http://127.0.0.1:5701/.

Cualquiera de los cinco, contra el mismo config:

<env count="3" seed="demo">
<sequence name="Payload"><gen type="number" value="10000000..99999999"/></sequence>
<sequence name="Card"><gen type="http" src="http://127.0.0.1:5701/" in="Payload"/></sequence>
<sequence name="Acct"><gen type="http" src="http://127.0.0.1:5701/"/></sequence>
</env>
./run demo.tdc
77737493 -> 777374935   |  cuenta: 71102997
14850763 -> 148507635   |  cuenta: 54325378
87262332 -> 872623327   |  cuenta: 37547759

Card pasó por el manejador — la carga volvió con su dígito de control. Acct se inventó solo a partir de la semilla. Cambie el puerto por 5702, 5703, 5704 o 5705 y la salida es carácter por carácter la misma.

Reproducibilidad: para qué sirve la semilla

El generador http es el único lugar donde TDC cede su garantía: el servicio decide los valores, así que el motor no puede prometer que volver a correr reproduzca. Su servicio sí puede prometerlo, y X-TDC-Seed es lo que lo hace posible.

La regla cabe en una línea: derive cada valor de la semilla, nunca de un reloj ni de un generador de números aleatorios.

accountFor(seed, i); // reproducible — same seed, same row, same answer
Math.random(); // not
new Date(); // not

La semilla que envía TDC es estable entre corridas y distinta para cada secuencia, así que dos secuencias http apuntadas al mismo servicio nunca reciben el mismo flujo.

Escrito así, la corrida se reproduce:

./run demo.tdc — dos veces
corrida 1:  71102997  54325378  37547759
corrida 2:  71102997  54325378  37547759

Calcule cada fila directamente, no itere

Fíjese en la forma de accountFor(seed, i): toma el índice de fila y devuelve el valor de esa fila, sin arrastrar estado entre llamadas. Es deliberado, y conviene copiarlo.

Un generador que recorre una secuencia — «llame a next() N veces» — tiene que llamarse en el orden correcto, desde el principio, exactamente una vez. Un servicio no está en posición de garantizar nada de eso: el motor puede reintentar una petición, y las peticiones pueden llegar a la vez. Una función sin estado de (seed, i) es inmune a todo ello, y no cuesta más escribirla.

La trampa: 32 bits en cinco lenguajes

Para que los cinco coincidan, tiene que coincidir la aritmética. Aquí es donde se rompe un port ingenuo, y se arregla con una línea por lenguaje:

LenguajeQué mantiene el hash en 32 bits
NodeMath.imul(h, prime) >>> 0 — un * normal pasaría por un double y perdería los bits bajos
Python& 0xFFFFFFFF — los enteros son de precisión arbitraria, nada se desborda solo
Javanada — la multiplicación de int ya da la vuelta
C#unchecked { … } — fuera de ahí, .NET lanza una excepción al desbordarse en vez de dar la vuelta
Rustwrapping_mul — un * normal entra en pánico al desbordar en una compilación de depuración

Olvídelo en Python y los números crecen sin fin, produciendo en silencio valores distintos de los demás. El propio motor de TDC tiene que resolver exactamente este problema — su PRNG está escrito con Math.imul y operaciones de 32 bits justamente para que los bindings de todos los lenguajes coincidan — así que la restricción no es un invento de este ejemplo.

Las implementaciones de arriba se ejecutaron y se compararon:

shasum -a 256 out.*.txt
875cd44fe86e15d7  out.cs.txt
875cd44fe86e15d7  out.java.txt
875cd44fe86e15d7  out.node.txt
875cd44fe86e15d7  out.py.txt

Idénticas, no meramente parecidas. Si lo lleva a un quinto lenguaje, haga la misma comprobación antes de confiar en él.

El manejador ya suele ser reproducible

Vale la pena notarlo: luhn() nunca toca la semilla. Un manejador calcula su respuesta a partir del valor que usted le mandó, así que es una función pura por naturaleza: misma entrada, misma salida, en cada corrida. Trabajar por la reproducibilidad le toca solo al modo fuente.

Antes de apuntarle TDC

Una lista corta, y cada punto sale de cómo falla este generador en la práctica:

  • Responda con exactamente N líneas. Una de más o de menos y la corrida se detiene con returned N line(s) for a batch of M. Esa comprobación existe porque un desajuste de longitud significa que las respuestas ya no cuadran con las filas — si no, sería corrupción silenciosa.
  • Conserve el orden. La línea i de la respuesta debe responder a la línea i de la petición. TDC no puede detectar un barajado; usted simplemente obtendría datos erróneos.
  • Los valores no deben contener un salto de línea, en ninguna dirección: el protocolo va por líneas, así que un salto incrustado rompe el conteo. Falla ruidosamente en vez de corromper, pero falla.
  • Sea seguro ante llamadas concurrentes, o corra la generación con --jobs 1. TDC no coordina a sus workers por usted.
  • Atienda todo el lote en una petición. No lo despliegue internamente en una llamada por valor; el lote es lo que mantiene esto rápido.

Vea también