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

Генератор http

Когда применять — когда значение должно прийти из логики, которой у TDC нет: настоящий алгоритм контрольной цифры, который вы уже написали, обращение к своей базе, любой расчёт, который тяжело выразить в конфиге. Вы поднимаете небольшой сервис, а TDC к нему обращается: движок становится клиентом вашего сервиса, а не хостом вашего кода. Это и есть точка расширения — всё, что вы спрячете за HTTP-адрес, становится частью ваших данных.

Два режима: он генерирует или он обрабатывает

Какой из них — решает один атрибут, и это по-настоящему разные работы:

inчто получает ваш сервисчто он делает
Источникнетничего, кроме счётчикапридумывает значения сам — работает как генератор
Обработчикестьваши значения, по одному на строкупреобразует присланное и возвращает обратно

Оба сразу, к одному и тому же сервису — первую колонку отдают и получают изменённой, вторая берётся из ниоткуда:

<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"/> <!-- обработчик -->
</sequence>

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

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

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

Как написать сам сервис

Полный рабочий сервис — на Node, Python и Java, с обоими режимами — и как сделать его воспроизводимым от сида, вынесены на отдельную страницу: Как написать сервис-генератор.

Колонка входных значений уходит одним запросом; ответ приходит по значению на строку, в том же порядке. Здесь сервис приводит каждое значение к верхнему регистру (a → A).
  • Aколонка входа — значения, которые дала ваша последовательность
  • Bваш сервис: TDC только обращается к нему, но не запускает
  • Cответ — одно значение на строку, в порядке отправки

Атрибуты

АтрибутЧто задаёт
srcадрес сервиса — http://127.0.0.1:5566/gen (локально, быстро) или внешний хост. https тоже работает
inпоследовательность, чьё значение уходит на каждой строке, — именно это делает сервис обработчиком. Без него сервис — источник: ничего не получает и придумывает каждое значение
on_errorfail (по умолчанию) — остановиться с внятным сообщением; empty — оставить ячейку пустой и продолжить
timeoutсколько секунд ждать один ответ, прежде чем сдаться. По умолчанию 30
secretключ, которым подписывается каждый запрос, чтобы сервис отличал TDC от всех прочих, кто дотягивается до порта. Три написания — см. Доказать, что запрос от TDC

in называет более раннюю последовательность — уходит то значение, что она дала на каждой строке.

Контракт, который реализует ваш сервис

Движок говорит по одному маленькому протоколу и ничего сверх него не ждёт:

  • POST на src, с заголовком X-TDC-Count: N — сколько значений нужно.
  • Content-Type: text/plain, ровно так, без параметра charset. Все пять реализаций шлют одну и ту же строку, поэтому сервис может сверять её буквально.
  • Тело — это N входных значений, по одному на строку, в порядке строк. Без in тело пустое, а N берётся из заголовка.
  • X-TDC-Input: N едет всегда, когда есть in, и говорит, сколько входных строк в теле. Именно он отличает обработчик от источника там, где тело этого не может: in, называющий колонку из одного пустого значения, шлёт пустое тело — байт в байт то же, что шлёт источник. Сервис, который заголовок игнорирует, читает тело как и раньше.
  • X-TDC-Seed — восемь шестнадцатеричных цифр, выведенных из seed прогона и имени последовательности: одно и то же значение каждый раз и разное для разных последовательностей, чтобы сервис, генерирующий из него, был воспроизводим сам по себе.
  • X-TDC-Timestamp и X-TDC-Signature едут только тогда, когда в конфигурации есть secret.
  • Ответ должен быть ровно N строк, в том же порядке — строка i отвечает на вход i. Обычный текст.

Всё структурное — забота вашего сервиса. Если внутри он работает с JSON, наружу отдаёт то одно поле, что вам нужно, текстом — TDC внутри держит всё строками.

Один запрос на колонку, а не на строку. Движок шлёт всю пачку разом, поэтому тысяча строк — это один запрос. Сервис, написанный «строка на входе — строка на выходе», тоже работает: он просто идёт циклом по строкам одного полученного запроса.

Почему тысяча строк — это один запрос. Построчная форма (слева) была бы тысячей обращений; TDC шлёт всю колонку одним (справа).
  • Aпо запросу на строку — так TDC НЕ делает; это был бы вызов на каждое значение
  • Bодин запрос на всю колонку — вся пачка, один обход по сети

Именно это держит скорость: цена — один обход по сети плюс работа самого сервиса, а не обход на каждое значение. Поэтому же http работает на in-memory движке (одна из шести форм, которые так делают), и его лучше держать для сервиса на вашей машине или для прогона, размер которого вы прикинули нарочно, — а не для миллиарда строк к далёкому адресу.

Целый сервис на пяти языках

Каждый из них полон и отвечает на оба режима: оборачивает то, что вы прислали, и придумывает значения, когда тело пустое. Выбирайте язык — ведут себя одинаково.

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

Запустите любой, направьте src на его порт и прогоните конфиг из раздела Два режима — все пять дадут один и тот же вывод.

Прочитайте это, прежде чем писать свой, — это не опционально

Сервисы выше — самое короткое, что работает. Они невоспроизводимы: прогоните конфиг дважды, и придуманные значения будут какими вздумается сервису.

→ Как написать сервис-генератор — вот страница, которая важна. Там, с рабочим кодом на всех языках:

  • как сделать прогон воспроизводимым через X-TDC-Seed, который шлёт TDC, — то единственное, что возвращает этому генератору его гарантию;
  • почему значение(сид, i), а не next() — сервис не может обещать порядок вызовов, и итератор тихо ломается при повторах и одновременных запросах;
  • ловушка 32 бит, из-за которой наивный перенос на Python молча расходится с Node и Java;
  • список перед стартом: точное число строк, порядок, переносы, одновременность, пачка.

Пропустите — и данные будут выглядеть нормально, но не будут воспроизводимыми. Это самый дорогой вид «неправильно».

Когда что-то ломается

Сервис вне контроля TDC, поэтому отказы обрабатываются, а не прячутся:

  • on_error="fail" (по умолчанию) останавливает прогон с сообщением, называющим последовательность и сервис — http service for sequence "Checked" at … returned 500. Пустая колонка в готовом файле — сюрприз хуже, чем внятная остановка.
  • on_error="empty" оставляет затронутую колонку пустой и доводит прогон до конца — для случая, когда нужен вывод «как получится». Дырки проверяете вы.
  • 429 (слишком много запросов) всегда останавливает, даже при empty. «Притормози» и «отдать целую колонку потоком» несовместимы, а продолжение тихо обрезало бы данные.
  • Сервис, который не отвечает, отсекается по timeout, а не вешает прогон.
  • Сервис, который заливает — отвечает сильно больше, чем по значению на строку, — обрезается на 64 МБ с ошибкой, а не читается в память до конца.
  • secret, который не читается — переменная не задана, файла нет — останавливает прогон до того, как уйдёт хоть один запрос, называя последовательность. Отправить партию неподписанной было бы ровно тем исходом, ради предотвращения которого атрибут и существует.

Доказать, что запрос от TDC

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

secret= это делает, ни разу не отправив ключ в сеть. Каждый запрос несёт метку времени и подпись, посчитанную из секрета; сервис считает то же самое своей копией ключа и сравнивает.

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

С запросом едут два дополнительных заголовка:

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

Внутрь подписи входит всё, что решает ответ: измените тело, счётчик, seed или минуту — и она перестанет сходиться. Проверка — несколько строк:

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

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

Где живёт ключ

НаписаниеОткуда читается
secret="env:TDC_HTTP_SECRET"переменная окружения — рекомендуемое
secret="file:~/.tdc/service.key"файл, пробелы по краям обрезаются; относительный путь — от конфигурации
secret="k7Fm2p…"само значение — работает и предупреждает (TDC284)

Литерал предупреждает, а не отказывает, потому что сервис на 127.0.0.1 на один вечер — живой случай. Но конфигурация уезжает в систему контроля версий, а ключ уезжает вместе с ней — ради этого и существуют два других написания. secret="" — ошибка: подпись без ключа подделает кто угодно.

Что подпись даёт и чего не даёт

  • Она доказывает, что отправитель владеет секретом и что запрос не переписали по дороге.
  • Метка времени — то, что делает перехваченный запрос бесполезным завтра. Насколько узкое это окно, решает ваш сервис. TDC шлёт свои настоящие часы, а не --now прогона, так что конфигурация, привязанная к прошлой дате, продолжает работать.
  • У секрета нет срока годности. Тест, который гоняют раз в квартал, подпишется верно ключом, положенным месяцы назад, — ради этого здесь подпись, а не протухающий токен.
  • Она не прячет содержимое. По обычному http:// тело и ответ видны тому, кто слушает канал. Если через сервис ходят настоящие секреты, нужен ещё и https:// — они решают разные задачи.
  • Подпись меняется каждый прогон, потому что меняется метка времени. На данные это не влияет: у http они и так никогда не были воспроизводимы.

Чего он не обещает

Это единственный генератор, который жертвует гарантиями, которые остальной TDC держит. Проговорите их себе, прежде чем тянуться к нему:

  • Невоспроизводимо. Значения решает сервис, поэтому seed ничего не гарантирует, и повторный прогон даёт другие данные. Конфиг с http никогда не считается воспроизводимым.
  • Порядок следует сервису, а не сиду.
  • Локально или скромные объёмы. Через интернет большой прогон — это большое число обращений наружу; это для сервиса на вашей машине или для прогона, размер которого вы прикинули нарочно. Не для миллиарда строк к публичному адресу.
  • Из библиотеки — только асинхронный путь. Сетевой вызов не может выйти из синхронной функции, поэтому toString() и writeFile() на http-конфиге бросают исключение. Берите toStringAsync() / writeFileAsync() — CLI так и делает. Если вы запускаете из командной строки, делать ничего не нужно.

См. также