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 cualquiera de cinco lenguajes, 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»;
  • X-TDC-Input: N está presente exactamente cuando quien llama envió entradas, y dice cuántas líneas trae el cuerpo. Leerlo es opcional y vale la pena — vea abajo;
  • responda con exactamente N líneas, en el mismo orden;
  • texto plano, no hace falta JSON en ninguna parte.

Compare los nombres de las cabeceras sin distinguir mayúsculas, como exige HTTP. Las cinco implementaciones no los escriben igual en el cable: la biblioteca HTTP de Python canoniza X-TDC-Count como X-Tdc-Count — Go también —, mientras que las otras cuatro envían la escritura literal. Todos los ayudantes que aparecen en esta página (self.headers.get en Python, getRequestHeaders().getFirst en Java, req.headers en Node) ya ignoran las mayúsculas. Un servicio que compare el nombre como una cadena corriente funcionará con cuatro tiempos de ejecución y no con el quinto.

Lea X-TDC-Input si puede

«Cuerpo vacío significa invente los valores» es cierto casi siempre, y falso en un caso: una configuración cuya columna en in= guarda un único valor vacío también envía un cuerpo vacío. Sin la cabecera los dos casos son indistinguibles, y un servicio que adivinaba «fuente» inventaba un valor donde le habían pedido procesar uno.

const declared = req.headers['x-tdc-input']; // undefined → no enviaron entradas
const inputs =
declared === undefined ? [] :
Number(declared) === 0 ? [] :
body.length === 0 ? [''] : body.split('\n');

Un servicio que ignore la cabecera sigue funcionando igual que antes — por eso esto es una cabecera y no un formato de cuerpo nuevo.

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:

<tdc>
<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>
<block>
<line><data>${{Payload}} -> ${{Card}} | cuenta: ${{Acct}}</data></line>
</block>
</tdc>
./run demo.tdc
10047634 -> 100476340   |  cuenta: 71102997
48577070 -> 485770705   |  cuenta: 54325378
44149883 -> 441498839   |  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 las cinco implementaciones coincidan — así que la restricción no es un invento de este ejemplo.

Los cinco servicios de arriba se arrancaron y recibieron la misma pregunta — ocho valores, una semilla — y sus respuestas se compararon:

for p in 5701 5702 5703 5704 5705; do
curl -s -X POST -H "X-TDC-Count: 8" -H "X-TDC-Seed: demo" --data-binary "" \
http://127.0.0.1:$p/ > out.$p.txt
done
shasum -a 256 out.*.txt
1d7bf9b2a4ef1ded8558cc0afd0ef18d15321a9da2398dc72c87cb8629c2ca2d  out.node.txt
1d7bf9b2a4ef1ded8558cc0afd0ef18d15321a9da2398dc72c87cb8629c2ca2d  out.py.txt
1d7bf9b2a4ef1ded8558cc0afd0ef18d15321a9da2398dc72c87cb8629c2ca2d  out.java.txt
1d7bf9b2a4ef1ded8558cc0afd0ef18d15321a9da2398dc72c87cb8629c2ca2d  out.cs.txt
1d7bf9b2a4ef1ded8558cc0afd0ef18d15321a9da2398dc72c87cb8629c2ca2d  out.rs.txt

Idénticas, no meramente parecidas. Si lo lleva a un sexto 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.

Si el servicio es alcanzable por alguien más

Un servicio generador suele hacer algo que usted no querría que hiciera un desconocido: calcular el hash de una contraseña, emitir un número de cuenta, gastar dinero en una llamada a un modelo. En 127.0.0.1 la protección es el propio socket. En cualquier otro sitio, pida que quien llama firme.

La configuración añade secret=; la petición lleva entonces dos cabeceras más, y el secreto no viaja nunca:

X-TDC-Timestamp: 1786000000
X-TDC-Signature: hex(HMAC-SHA256(secret, timestamp \n seed \n count \n body))

Verificar son las mismas pocas líneas en cualquier lenguaje: recalcular y comparar.

const mine = crypto
.createHmac('sha256', process.env.TDC_HTTP_SECRET)
.update(`${ts}\n${seed}\n${count}\n${body}`)
.digest('hex');
if (mine !== req.headers['x-tdc-signature']) return res.status(401).end();

Tres cosas que conviene saber antes de escribir esa comprobación:

  • La ventana la elige usted. TDC estampa su reloj real y nada más; cuán vieja puede ser una petición antes de que la rechace es su política. Unos minutos es un valor por omisión sensato, y exige que los relojes de ambas máquinas coincidan más o menos.
  • El secreto no caduca, así que una suite que corre una vez por trimestre sigue firmando bien. Esa es la razón de una firma y no de un token.
  • Firmar no es cifrar. Por http:// simple el cuerpo sigue siendo legible para quien esté en el camino. Use también https:// cuando los valores mismos sean sensibles.

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