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:
in | qué recibe su servicio | qué hace | |
|---|---|---|---|
| Fuente | ausente | nada más que un conteo | inventa los valores él mismo — actúa como generador |
| Manejador | presente | sus valores, uno por línea | transforma 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>
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.
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.
- 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
| Atributo | Qué define |
|---|---|
src | la URL del servicio — http://127.0.0.1:5566/gen (local, rápido) o un host público. https también sirve |
in | la 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_error | fail (por omisión) — detenerse con un mensaje claro; o empty — dejar la celda vacía y seguir |
timeout | segundos a esperar por una respuesta antes de rendirse. Por omisión 30 |
secret | la 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:
POSTasrc, con una cabeceraX-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
Nvalores de entrada, uno por línea, en orden de fila. Sinin, el cuerpo está vacío yNviene de la cabecera. X-TDC-Input: Nviaja siempre que hayin, 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: uninque 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-Seedson 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-TimestampyX-TDC-Signatureviajan solo cuando la configuración llevasecret.- La respuesta debe ser exactamente
Nlí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.
- 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.
- Node.js
- Python
- Java
- C#
- Rust
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');
from http.server import BaseHTTPRequestHandler, HTTPServer
class H(BaseHTTPRequestHandler):
def do_POST(self):
n = int(self.headers.get("Content-Length", 0))
sent = self.rfile.read(n).decode() if n else ""
count = int(self.headers.get("X-TDC-Count", "0"))
if sent == "":
out = [f"SRC-{i:03d}" for i in range(count)] # source
else:
out = [f"[{line} ok]" for line in sent.split("\n")] # handler
body = "\n".join(out).encode()
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
HTTPServer(("127.0.0.1", 5802), H).serve_forever()
import com.sun.net.httpserver.HttpServer;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class S3 {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 5803), 0);
server.createContext("/", exchange -> {
String sent = new String(exchange.getRequestBody().readAllBytes(),
StandardCharsets.UTF_8);
String raw = exchange.getRequestHeaders().getFirst("X-TDC-Count");
int count = Integer.parseInt(raw == null ? "0" : raw);
List<String> out = new ArrayList<>();
if (sent.isEmpty()) {
for (int i = 0; i < count; i++) out.add(String.format("SRC-%03d", i)); // source
} else {
for (String line : sent.split("\n", -1)) out.add("[" + line + " ok]"); // handler
}
byte[] body = String.join("\n", out).getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) { os.write(body); }
});
server.start();
}
}
using System.Net;
using System.Text;
var server = new HttpListener();
server.Prefixes.Add("http://127.0.0.1:5804/");
server.Start();
while (true)
{
HttpListenerContext ctx = server.GetContext();
using var reader = new StreamReader(ctx.Request.InputStream, Encoding.UTF8);
string sent = reader.ReadToEnd();
int count = int.Parse(ctx.Request.Headers["X-TDC-Count"] ?? "0");
IEnumerable<string> lines = sent.Length == 0
? Enumerable.Range(0, count).Select(i => $"SRC-{i:D3}") // fuente
: sent.Split('\n').Select(line => $"[{line} ok]"); // manejador
byte[] body = Encoding.UTF8.GetBytes(string.Join("\n", lines));
ctx.Response.ContentLength64 = body.Length;
ctx.Response.OutputStream.Write(body);
ctx.Response.Close();
}
use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpListener;
fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:5805")?;
for stream in listener.incoming() {
let mut stream = stream?;
let mut reader = BufReader::new(stream.try_clone()?);
let (mut length, mut count) = (0usize, 0usize);
loop {
let mut line = String::new();
if reader.read_line(&mut line)? == 0 || line == "\r\n" {
break;
}
let lower = line.to_ascii_lowercase();
if let Some(v) = lower.strip_prefix("content-length:") {
length = v.trim().parse().unwrap_or(0);
} else if let Some(v) = lower.strip_prefix("x-tdc-count:") {
count = v.trim().parse().unwrap_or(0);
}
}
let mut sent = vec![0u8; length];
reader.read_exact(&mut sent)?;
let sent = String::from_utf8_lossy(&sent);
let out: Vec<String> = if sent.is_empty() {
(0..count).map(|i| format!("SRC-{i:03}")).collect() // source
} else {
sent.split('\n').map(|line| format!("[{line} ok]")).collect() // handler
};
let body = out.join("\n");
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: {}\r\n\r\n{body}",
body.len()
)?;
}
Ok(())
}
Sin crates: la biblioteca estándar trae un listener TCP, y una petición HTTP son unas cuantas líneas de cabeceras seguidas de un cuerpo.
Arranque uno, apunte src a su puerto y corra el config de
Dos modos — los cinco producen la misma salida.
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-Seedque TDC le envía — lo único que le devuelve a este generador su garantía; - por qué
valor(seed, i)y nuncanext()— 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 conempty. «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
timeouten 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
secretque 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
| Forma | De 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
--nowde 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énhttps://— resuelven problemas distintos. - La firma cambia en cada corrida, porque cambia la marca de tiempo. Eso no afecta a los
datos, que con
httpnunca 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
seedno garantiza nada y volver a correr da datos distintos. Un config que usahttpnunca 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()ywriteFile()lanzan sobre una configuración conhttp. UsetoStringAsync()/writeFileAsync()— la CLI ya lo hace. Desde la línea de comandos no hay nada que hacer.
Vea también
- Escribir un generador de servicio — servicios funcionando en Node, Python y Java
- Visión general de generadores
- Códigos de error —
TDC065–TDC069,TDC284y los fallos en corrida