Saltar al contenido principal

El generador http

Úselo cuando el valor tiene que venir de una lógica que TDC no tiene: un algoritmo de dígito de control real que usted ya escribió, una consulta a su propia base, cualquier cálculo que sería doloroso expresar en el config. Usted levanta un pequeño servicio y TDC lo llama: el motor se vuelve cliente de su servicio, no anfitrión de su código. Ese es el punto de extensión — cualquier cosa que ponga detrás de un endpoint HTTP se vuelve parte de sus datos.

Dos modos: genera, o procesa

Un solo atributo decide cuál, y son trabajos genuinamente distintos:

inqué recibe su servicioqué hace
Fuenteausentenada más que un conteoinventa los valores él mismo — actúa como generador
Manejadorpresentesus valores, uno por líneatransforma lo que envió y lo devuelve

Los dos a la vez, contra el mismo servicio — la primera columna se entrega y vuelve cambiada, la segunda se saca de la nada:

<sequence name="City">
<gen type="text" value="Paris,Berlin,Tokyo" order="sequential"/>
</sequence>

<sequence name="Handled">
<gen type="http" src="http://127.0.0.1:5599/gen" in="City"/> <!-- manejador -->
</sequence>

<sequence name="Made">
<gen type="http" src="http://127.0.0.1:5599/gen"/> <!-- fuente -->
</sequence>
./run modes.tdc
Paris  ->  [Paris ok]    |  Made: SRC-000
Berlin ->  [Berlin ok]   |  Made: SRC-001
Tokyo  ->  [Tokyo ok]    |  Made: SRC-002

El modo manejador es el más útil de los dos, y el más fácil de pasar por alto: deja que un servicio que usted ya tiene termine un valor que TDC empezó — validarlo, agregarle un dígito de control, buscarlo, traducirlo — en vez de reemplazar el generador entero.

Su servicio distingue los modos por el cuerpo de la petición: cuerpo vacío es modo fuente, y la cabecera X-TDC-Count dice cuántos valores inventar.

Cómo escribir el servicio

Un servicio completo y funcionando — en Node, Python y Java, con ambos modos — y cómo hacerlo reproducible a partir de la semilla, viven en su propia página: Escribir un generador de servicio.

La columna de entradas se envía en una sola petición; la respuesta vuelve un valor por fila, en el mismo orden. Aquí el servicio pasa cada valor a mayúsculas (a → A).
  • Ala columna de entrada — los valores que produjo su secuencia
  • Bsu servicio: TDC solo habla con él, nunca lo ejecuta
  • Cla respuesta — un valor por fila, en el orden enviado

Atributos

AtributoQué define
srcla URL del servicio — http://127.0.0.1:5566/gen (local, rápido) o un host público. https también sirve
inla secuencia cuyo valor se envía en cada fila — esto es lo que convierte al servicio en manejador. Omítalo y el servicio es una fuente: no recibe nada e inventa cada valor
on_errorfail (por omisión) — detenerse con un mensaje claro; o empty — dejar la celda vacía y seguir
timeoutsegundos a esperar por una respuesta antes de rendirse. Por omisión 30
secretla clave con la que se firma cada petición, para que su servicio distinga a TDC de cualquier otro que alcance el puerto. Tres formas — vea Probar que la petición viene de TDC

in nombra una secuencia anterior — se envía el valor que produjo en cada fila.

El contrato que implementa su servicio

El motor habla un protocolo pequeño y no espera nada más de vuelta:

  • POST a src, con una cabecera X-TDC-Count: N — cuántos valores se quieren.
  • Content-Type: text/plain, exactamente eso, sin parámetro charset. Las cinco implementaciones envían la misma cadena, así que un servicio puede compararla al pie de la letra.
  • El cuerpo son los N valores de entrada, uno por línea, en orden de fila. Sin in, el cuerpo está vacío y N viene de la cabecera.
  • X-TDC-Input: N viaja siempre que hay in, y dice cuántas líneas de entrada trae el cuerpo. Es lo que distingue un manejador de una fuente donde el cuerpo no puede: un in que nombra una columna con un solo valor vacío envía un cuerpo vacío — byte por byte lo mismo que envía una fuente. Un servicio que ignore la cabecera lee el cuerpo como siempre.
  • X-TDC-Seed son ocho dígitos hexadecimales derivados de la semilla de la corrida y del nombre de la secuencia: el mismo valor cada vez, y distinto para cada secuencia, para que un servicio que genere a partir de él sea reproducible por su cuenta.
  • X-TDC-Timestamp y X-TDC-Signature viajan solo cuando la configuración lleva secret.
  • La respuesta debe ser exactamente N líneas, en el mismo orden — la línea i responde a la entrada i. Texto plano.

Todo lo estructurado es trabajo de su servicio. Si por dentro trabaja con JSON, devuelve el único campo que usted quiere, como texto — TDC mantiene todo como cadenas por dentro.

Una petición por columna, no por fila. El motor envía todo el lote de una vez, así que mil filas es una petición. Un servicio escrito «una línea entra, una línea sale» también funciona: solo recorre en bucle las líneas de la única petición que recibe.

Por qué mil filas es una petición. La forma por fila (izquierda) serían mil viajes de ida y vuelta; TDC envía toda la columna en uno (derecha).
  • Auna petición por fila — lo que TDC NO hace; sería una llamada por valor
  • Buna petición para toda la columna — todo el lote, un viaje de ida y vuelta

Esto es lo que lo mantiene rápido: el costo es un viaje de ida y vuelta más el trabajo del propio servicio, no un viaje por valor. También por eso http corre en el motor en memoria (una de las seis formas que lo hacen) y conviene reservarlo para un servicio en su propia máquina, o una corrida que usted dimensionó a propósito — no mil millones de filas contra un endpoint lejano.

Un servicio entero, en cinco lenguajes

Cada uno está completo y responde a ambos modos: envuelve lo que usted manda e inventa valores cuando el cuerpo viene vacío. Elija su lenguaje — se comportan igual.

import { createServer } from 'node:http';

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 sent = Buffer.concat(chunks).toString('utf8');

const out =
sent === ''
? Array.from({ length: count }, (_, i) => 'SRC-' + String(i).padStart(3, '0')) // source
: sent.split('\n').map((line) => '[' + line + ' ok]'); // handler

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

Arranque uno, apunte src a su puerto y corra el config de Dos modos — los cinco producen la misma salida.

Lea esto antes de escribir el suyo — no es opcional

Los servicios de arriba son lo más corto que funciona. No son reproducibles: corra el config dos veces y los valores inventados serán los que al servicio se le ocurran.

→ Escribir un generador de servicio es la página que importa. Allí, con código funcionando en los cinco lenguajes:

  • cómo hacer que una corrida se reproduzca usando el X-TDC-Seed que TDC le envía — lo único que le devuelve a este generador su garantía;
  • por qué valor(seed, i) y nunca next() — un servicio no puede prometer el orden de las llamadas, y un iterador se rompe en silencio ante reintentos y concurrencia;
  • la trampa de los 32 bits que hace que un port ingenuo a Python discrepe de Node y Java;
  • la lista previa al vuelo: conteo exacto de líneas, orden, saltos de línea, concurrencia, lote.

Sáltesela y sus datos se verán bien y no serán reproducibles. Ese es el tipo caro de estar equivocado.

Cuando algo falla

El servicio está fuera del control de TDC, así que los fallos se manejan, no se ocultan:

  • on_error="fail" (el valor por omisión) detiene la corrida con un mensaje que nombra la secuencia y el servicio — http service for sequence "Checked" at … returned 500. Una columna en blanco en un archivo terminado es peor sorpresa que una parada clara.
  • on_error="empty" deja en blanco la columna afectada y termina, para cuando lo que quiere es una salida de mejor esfuerzo. Los huecos los revisa usted.
  • 429 (límite de tasa) siempre detiene, incluso con empty. «Más despacio» y «transmitir una columna entera» no se pueden reconciliar, y seguir truncaría los datos en silencio.
  • Un servicio que nunca responde se corta por timeout en vez de colgar la corrida.
  • Un servicio que inunda — respondiendo mucho más que un valor por línea — se corta a los 64 MB con un error, en vez de leerse en memoria hasta el final.
  • Un secret que no se puede leer — variable sin definir, archivo ausente — detiene la corrida antes de que salga ninguna petición, nombrando la secuencia. Enviar el lote sin firmar sería justo el desenlace que el atributo existe para evitar.

Probar que la petición viene de TDC

Un servicio que responde peticiones http hace trabajo de verdad — calcula el hash de una contraseña, emite un número de cuenta, llama a un modelo que cuesta dinero. Si algo más que su propia máquina lo alcanza, debería poder distinguir las peticiones de TDC de las ajenas.

secret= hace eso sin poner nunca la clave en la red. Cada petición lleva una marca de tiempo y una firma calculada a partir del secreto; el servicio recalcula lo mismo con su propia copia y compara.

<sequence name="Hashed">
<gen type="http" src="https://svc.example.com/hash" in="Password"
secret="env:TDC_HTTP_SECRET"/>
</sequence>

Con la petición viajan dos cabeceras más:

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

Dentro de la firma está todo lo que decide la respuesta: cambie el cuerpo, la cuenta, la semilla o el minuto y deja de coincidir. Verificarla son unas pocas líneas:

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();

Las cinco implementaciones producen la misma firma para la misma petición, así que un solo servicio acepta peticiones de cualquiera de ellas.

Dónde vive la clave

FormaDe dónde se lee
secret="env:TDC_HTTP_SECRET"una variable de entorno — la recomendada
secret="file:~/.tdc/service.key"un archivo, sin espacios al borde; las rutas relativas se resuelven junto a la configuración
secret="k7Fm2p…"el valor mismo — funciona, y advierte (TDC284)

El literal advierte en vez de fallar porque un servicio en 127.0.0.1 por una tarde es un uso real. Pero una configuración va al control de versiones y la clave va con ella: por eso existen las otras dos formas. secret="" es un error: firmar con nada produce una firma que cualquiera puede falsificar.

Qué hace la firma y qué no

  • Prueba que quien envía tiene el secreto, y que la petición no fue alterada en el camino.
  • La marca de tiempo es lo que vuelve inútil mañana una petición capturada. Qué tan estrecha es esa ventana lo decide su servicio. TDC envía su reloj real, no el --now de la corrida, así que una configuración fijada a una fecha pasada sigue funcionando.
  • El secreto no caduca. Una prueba que corre una vez por trimestre firma bien con una clave puesta meses atrás — por eso aquí hay una firma y no un token que expira.
  • No oculta el contenido. Por http:// simple, el cuerpo y la respuesta son visibles para quien escuche el canal. Si por el servicio pasan secretos de verdad, use también https:// — resuelven problemas distintos.
  • La firma cambia en cada corrida, porque cambia la marca de tiempo. Eso no afecta a los datos, que con http nunca fueron reproducibles.

Lo que no promete

Este es el único generador que cede garantías que el resto de TDC mantiene. Dígaselas a sí mismo antes de recurrir a él:

  • No reproducible. El servicio decide los valores, así que seed no garantiza nada y volver a correr da datos distintos. Un config que usa http nunca se trata como reproducible.
  • El orden sigue al servicio, no a la semilla.
  • Local o volúmenes modestos. Por internet, una corrida grande es una gran cantidad de llamadas salientes; esto es para un servicio en su propia máquina, o una corrida que usted dimensionó a propósito. No para mil millones de filas contra un endpoint público.
  • Desde la biblioteca, solo el camino asíncrono. Una llamada de red no puede salir de una función síncrona, así que toString() y writeFile() lanzan sobre una configuración con http. Use toStringAsync() / writeFileAsync() — la CLI ya lo hace. Desde la línea de comandos no hay nada que hacer.

Vea también