Перейти к основному содержимому

Как написать сервис-генератор

Генератор http отдаёт решение о значениях сервису, который написали вы. Эта страница — вторая половина: как написать такой сервис на Node, Python или Java, чтобы он был быстрым, корректным и — вот это упускают все — воспроизводимым.

Ниже по одному рабочему сервису на язык. Каждый закрывает оба режима: придумывает номера счетов, когда у него просят значения, и приписывает контрольную цифру Луна, когда ему значения присылают.

Что сервис обязан делать

Полный контракт — на странице генератора. Чтобы написать сервис, хватает четырёх строк из него:

  • приходит POST с заголовками X-TDC-Count: N и X-TDC-Seed: <hex>;
  • тело — это N значений, по одному на строку, либо пустое, что значит «придумай их»;
  • ответить нужно ровно N строками, в том же порядке;
  • обычный текст, никакого JSON нигде не требуется.

Сервис на пяти языках

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

Запуск: node service.mjs, затем направьте src на http://127.0.0.1:5701/.

Любой из пяти, на одном и том же конфиге:

<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   |  счёт: 71102997
14850763 -> 148507635   |  счёт: 54325378
87262332 -> 872623327   |  счёт: 37547759

Card прошла через обработчик — полезная часть вернулась с контрольной цифрой. Acct придумана из одного лишь сида. Поменяйте порт на 5702, 5703, 5704 или 5705 — вывод будет символ в символ тем же.

Воспроизводимость: зачем нужен сид

Генератор http — единственное место, где TDC жертвует своей гарантией: значения решает сервис, поэтому движок не может обещать, что повторный прогон воспроизведётся. Ваш сервис — может, и X-TDC-Seed это и делает возможным.

Правило в одну строку: выводите каждое значение из сида, никогда — из часов или генератора случайных чисел.

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

Сид, который шлёт TDC, стабилен между прогонами и разный для каждой последовательности, поэтому две http-последовательности к одному сервису никогда не получат один и тот же поток.

Написанный так прогон воспроизводится:

./run demo.tdc — дважды
прогон 1:  71102997  54325378  37547759
прогон 2:  71102997  54325378  37547759

Считайте строку напрямую, а не перебором

Обратите внимание на форму accountFor(seed, i): она принимает номер строки и возвращает значение этой строки, не таща состояние между вызовами. Это сделано нарочно, и это стоит перенять.

Генератор, который идёт по последовательности — «вызови next() N раз», — обязан вызываться в правильном порядке, с начала и ровно один раз. Сервис не в том положении, чтобы это гарантировать: движок может повторить запрос, а запросы могут прийти одновременно. Функция без состояния от (сид, i) ко всему этому невосприимчива, а писать её не сложнее.

Ловушка: 32 бита на пяти языках

Чтобы все пять сошлись, должна сойтись арифметика. Именно здесь ломается наивный перенос, и лечится это одной строкой на язык:

ЯзыкЧто удерживает хеш в 32 битах
NodeMath.imul(h, prime) >>> 0 — обычное * прошло бы через double и потеряло младшие биты
Python& 0xFFFFFFFF — целые здесь неограниченной длины, само ничего не переполнится
Javaничего — умножение int уже заворачивается
C#unchecked { … } — вне него .NET на переполнении бросает исключение, а не заворачивает
Rustwrapping_mul — обычное * в отладочной сборке паникует на переполнении

Пропустите это в Python — числа будут расти бесконечно и тихо дадут значения, отличные от остальных. Движку TDC приходится решать ровно ту же задачу: его генератор случайных чисел написан через Math.imul и 32-битные операции именно ради того, чтобы биндинги на все реализации совпадали, — так что ограничение это не выдумка примера.

Четыре реализации выше были запущены и сверены:

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

Идентичны, а не «похожи». Если переносите это на пятый язык — сделайте ту же проверку, прежде чем доверять.

Обработчик обычно воспроизводим и так

Стоит заметить: luhn() вообще не трогает сид. Обработчик считает ответ из того значения, что вы ему прислали, то есть он чистая функция по своей природе: тот же вход — тот же выход, в любом прогоне. Работать ради воспроизводимости приходится только режиму источника.

Прежде чем направлять на него TDC

Короткий список, и каждый пункт взят из того, как этот генератор ломается на деле:

  • Отвечайте ровно N строками. Одной больше или меньше — и прогон встанет с returned N line(s) for a batch of M. Проверка существует потому, что расхождение в длине означает: ответы больше не совпадают со строками, иначе была бы тихая порча.
  • Держите порядок. Строка i ответа должна отвечать строке i запроса. Перестановку TDC обнаружить не может — вы просто получите неверные данные.
  • В значениях не должно быть перевода строки, ни туда, ни обратно: протокол построчный, и перенос внутри значения ломает счёт. Падает громко, а не портит, — но падает.
  • Будьте безопасны при одновременных вызовах либо запускайте генерацию с --jobs 1. Своих воркеров TDC за вас не согласует.
  • Обрабатывайте всю пачку одним запросом. Не разворачивайте её внутри в вызов на каждое значение — именно пачка держит скорость.

См. также