8 lines
27 KiB
JSON
8 lines
27 KiB
JSON
{
|
||
"index": 56,
|
||
"slug": "editorial-2026-06-mechanism-multi-runtime",
|
||
"title": "Один payload, три runtime: как сохранить смысл на границе",
|
||
"excerpt": "Похожая структура данных не делает PHP, JavaScript и D взаимозаменяемыми. Разбираем контракт type, error и time, fail-closed проверку и границы вывода.",
|
||
"contentHtml": "<p>Наблюдаемый симптом — один и тот же заказ получает разные решения. В одном заказе три участника видят почти одинаковый JSON: <code>amount</code>, <code>currency</code> и <code>status</code>. PHP считает <code>status=ready</code> успешным завершением. JavaScript проверяет только наличие строки и продолжает обработку. D ждёт отдельный код результата и оставляет операцию незавершённой. На экране это один payload, но решения уже расходятся.</p>\n<p>Цена расхождения появляется после сбоя. Клиент может повторить уже принятый заказ, worker — пропустить отказ, а оператор — связать события по совпавшим полям, хотя они относятся к разным стадиям. Попытка быстро исправить ситуацию обычно выглядит невинно: привести значение к строке, подставить код по умолчанию или включить retry. Но каждое молчаливое преобразование переносит неопределённость дальше по цепочке.</p>\n<p>Здесь полезно проверять не сходство объектов, а сохранение смысла. Минимальная граница состоит из версии схемы, именованного типа значения, режима ошибки и шкалы времени. Если хотя бы одна ось не описана, адаптер должен остановиться с конкретной причиной. Такой подход не делает языки одинаковыми; он показывает, в каком месте они перестают быть сопоставимыми.</p>\n<h2>Сначала отделим форму от смысла</h2>\n<p>JSON описывает синтаксическую форму обмена, но не решает, что означает поле <code>amount</code> или можно ли повторить операцию. Число <code>4200</code> может быть суммой в копейках, лимитом или идентификатором. Строка <code>ready</code> может быть именем состояния, текстом интерфейса или значением, которое случайно прошло нестрогое сравнение. Одинаковый shape не является доказательством одинакового поведения.</p>\n<p>Поэтому полезная запись на границе называет смысл явно. В учебном примере <code>value.tag</code> фиксирует тип полезной нагрузки, <code>amountMinor</code> — целое число в минимальных денежных единицах, а <code>currency</code> — код валюты. Это проектные решения конкретного примера, а не свойства PHP, JavaScript или D. Их нужно закрепить в API-схеме и тестах своего продукта.</p>\n<p>Версия схемы отвечает на вопрос «какую форму мы сейчас проверяем». Она не заменяет версию runtime и библиотеки. Если PHP меняет правила приведения скаляров, а JavaScript или D иначе представляют ошибку, одна версия JSON не устраняет различие. Версия нужна, чтобы не подменять новый договор старым объектом; остальные зависимости проверяются отдельно.</p>\n<figure><img src=\"/assets/editorial/2026/multi-runtime-2026-error-type-time-matrix.svg\" alt=\"Матрица проверки type, error, time и adapter для PHP, JavaScript и D\" loading=\"lazy\" /><figcaption>Четыре строки отвечают на четыре разных вопроса: назван ли тип значения, описан ли режим ошибки, определена ли шкала времени и выполнено ли точное сопоставление. Красная точка означает остановку проверки, а не дефект конкретного языка.</figcaption></figure>\n<h2>Четыре независимые оси контракта</h2>\n<p><code>type</code> должен быть именованным тегом, а не выводом из соседних полей. Без него адаптер не знает, является ли <code>4200</code> суммой или лимитом. Тег также не заменяет проверку содержимого: после <code>order-ready</code> всё равно нужно проверить диапазон суммы, код валюты и обязательные поля.</p>\n<p><code>error</code> описывает не текст сообщения, а режим завершения. В примере есть <code>semantics</code>, <code>code</code> и <code>retry</code>. Пара <code>code: null</code> и <code>semantics: named-envelope</code> означает только отсутствие кода в этой записи. Она не доказывает, что в системе не было ошибки. Поле <code>retry</code> сообщает намерение или результат политики, но не делает повтор безопасным без ключа идемпотентности и правила побочных эффектов.</p>\n<p><code>time</code> должен назвать шкалу и обе границы интервала. В локальном примере используются фиксированные логические такты: по ним можно проверить порядок <code>opened <= closed</code>. Это не миллисекунды и не измерение производительности. Для latency понадобятся источник часов, единицы измерения, точки старта и окончания, а также правило, где именно начинается операция.</p>\n<p><code>adapter</code> фиксирует перевод между внутренним представлением и этим envelope. Участник не может объявить себя совместимым по одному имени: проверяются версия, tag, режим ошибки, шкала времени и способ mapping. Если в одном месте происходит coercion, это отдельное правило преобразования с тестами, а не «точное» сопоставление.</p>\n<h2>Что действительно различается в runtime</h2>\n<p>Официальная документация PHP прямо описывает важную ловушку: по умолчанию скалярные значения могут быть приведены к объявленному типу, а <code>declare(strict_types=1)</code> меняет проверку для вызовов из конкретного файла. Несовпадение может закончиться <code>TypeError</code>. Поэтому PHP-тип параметра нельзя автоматически считать описанием внешнего JSON-контракта: поведение зависит от места вызова и от того, где стоит граница сериализации.</p>\n<p>Спецификация ECMAScript описывает language types и completion records JavaScript. Это модель выполнения программы, а не готовый HTTP-envelope с полями <code>code</code> и <code>retry</code>. Преобразование исключения или результата в такой envelope — решение адаптера. Его нельзя приписать самому JSON или назвать общим свойством всех трёх runtime.</p>\n<p>Спецификация D описывает собственную модель ошибок и раскрутки стека. Она помогает понять поведение D-кода внутри его среды, но не задаёт внешний договор с PHP и JavaScript. На транспортной границе нужно отдельно решить, какие ошибки становятся <code>error.code</code>, что происходит с незавершённой операцией и кто может инициировать повтор.</p>\n<p>Это различие важно для расследования. Фраза «язык вернул ошибку» слишком широка. Нужно записать наблюдаемый слой: тип значения до сериализации, байты или JSON на транспорте, результат десериализации, решение адаптера и побочный эффект операции. Только так можно понять, где исчезло поле или возникло неявное приведение.</p>\n<h2>Воспроизводимая fail-closed проверка</h2>\n<p>Ниже — самостоятельный пример для Node.js 18+. Он проверяет объект в памяти и не вызывает сеть, часы, PHP или D. Все значения внутри него учебные. Смысл функции в другом: неполная запись получает именованную причину остановки, а положительный результат появляется только при полном наборе независимых признаков.</p>\n<pre><code>const validRecord = {\n schemaVersion: 'boundary-1',\n operation: 'create-order',\n value: { tag: 'order-ready', amountMinor: 4200, currency: 'RUB' },\n error: { semantics: 'named-envelope', code: null, retry: 'not-requested' },\n time: { basis: 'fixed-logical-ticks', opened: 100, closed: 108 }\n};\n\nconst adapters = [\n { model: 'php', version: 'boundary-1', valueTag: 'order-ready',\n errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n { model: 'javascript', version: 'boundary-1', valueTag: 'order-ready',\n errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n { model: 'd', version: 'boundary-1', valueTag: 'order-ready',\n errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' }\n];\n\nfunction checkEnvelope(record, participants) {\n if (!record || record.schemaVersion !== 'boundary-1' ||\n record.operation !== 'create-order') {\n return 'stop-incomplete-contract';\n }\n const requiredModels = new Set(['php', 'javascript', 'd']);\n const actualModels = new Set(participants.map((item) => item.model));\n if (actualModels.size !== 3 ||\n [...requiredModels].some((model) => !actualModels.has(model))) {\n return 'stop-incomplete-adapter-set';\n }\n if (record.value?.tag !== 'order-ready' ||\n !Number.isInteger(record.value?.amountMinor) ||\n typeof record.value?.currency !== 'string') {\n return 'stop-incomplete-value';\n }\n if (record.error?.semantics !== 'named-envelope' ||\n !Object.hasOwn(record.error, 'code') ||\n typeof record.error.retry !== 'string') {\n return 'stop-incomplete-error';\n }\n if (record.time?.basis !== 'fixed-logical-ticks' ||\n !Number.isInteger(record.time.opened) ||\n !Number.isInteger(record.time.closed) ||\n record.time.closed < record.time.opened) {\n return 'stop-undetermined-time-boundary';\n }\n const valid = participants.every((item) =>\n item.version === record.schemaVersion &&\n item.valueTag === record.value.tag &&\n item.errorSemantics === record.error.semantics &&\n item.timeBasis === record.time.basis &&\n item.mapping === 'exact'\n );\n return valid ? 'accepted-fixed-contract' : 'stop-incomparable-adapter';\n}\n\nconst copy = () => JSON.parse(JSON.stringify(validRecord));\nconsole.log(checkEnvelope(validRecord, adapters));\nconst unknownTag = copy();\nunknownTag.value.tag = 'order-paid';\nconsole.log(checkEnvelope(unknownTag, adapters));\nconst mismatchedAdapter = adapters.map((item) => ({ ...item }));\nmismatchedAdapter[1].valueTag = 'order-paid';\nconsole.log(checkEnvelope(validRecord, mismatchedAdapter));\n\n// accepted-fixed-contract\n// stop-incomplete-value\n// stop-incomparable-adapter</code></pre>\n<p>Проверка начинается с операции и набора участников, поэтому три записи одного и того же runtime не проходят как «три среды». Затем она валидирует значение, ошибку и время по отдельности. Вызов <code>Object.hasOwn</code> не даёт превратить отсутствие кода в неявный default. На последнем шаге адаптеры сравниваются с записью, а не друг с другом: взаимное совпадение трёх одинаково ошибочных переводов не считается доказательством.</p>\n<p>Пример можно запустить, сохранив код в чистый файл и выполнив <code>node checker.mjs</code>. Для настоящего API следует заменить учебную запись схемой продукта, добавить проверку неизвестных полей и зафиксировать правила сериализации. Результат функции не является сертификатом совместимости: он показывает, что именно проверено и где проверка отказалась делать вывод.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Минимальная карта расследования неоднозначного payload</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Вероятная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Одинаковый shape даёт разные решения</td><td>Не названы tag, версия или единицы полей</td><td>Сверить обязательные поля до бизнес-ветки</td><td>Расширить envelope; не выводить смысл из shape</td></tr><tr><td>Один участник бросает ошибку, другие возвращают объект</td><td>Смешаны режимы завершения</td><td>Сопоставить semantics, code и момент завершения</td><td>Задокументировать адаптер или остановить mapping</td></tr><tr><td>После таймаута создаётся второй заказ</td><td>retry есть, а идемпотентность не определена</td><td>Повторить вызов с тем же ключом на тестовой записи</td><td>Запретить автоматический повтор до правила эффекта</td></tr><tr><td>Отчёт говорит «быстрее», но метрики нет</td><td>Логические такты приняты за latency</td><td>Проверить basis, часы, единицы и границы</td><td>Удалить вывод о скорости или завести отдельное измерение</td></tr><tr><td>Неполный объект считается успешным</td><td>Валидатор подставляет default</td><td>Удалить поле и проверить точную stop-причину</td><td>Сделать обязательность явной и покрыть missing-case</td></tr></tbody></table></div>\n<h2>Отрицательный путь должен быть частью договора</h2>\n<p>Положительная запись показывает только счастливую ветку. Дисциплину проверяют изменения, которые инженер обязан отвергнуть. Удалите <code>schemaVersion</code> или замените <code>operation</code>: функция возвращает <code>stop-incomplete-contract</code>. Она не угадывает версию по текущей сборке и не пытается «помочь» вызывающему коду.</p>\n<p>Замените <code>value.tag</code> на неизвестное значение. Ожидаемая причина — <code>stop-incomplete-value</code>: запись больше не соответствует выбранному типу. Чтобы проверить несовместимый адаптер, измените у JavaScript-участника <code>valueTag</code>. Тогда функция вернёт <code>stop-incomparable-adapter</code>. То же происходит, если JavaScript сообщает <code>thrown-value</code> вместо <code>named-envelope</code>. Нельзя объявить эти варианты равными только потому, что оба заканчиваются словом «ошибка».</p>\n<p>Поставьте <code>time.closed: null</code> или поменяйте <code>basis</code> на <code>wall-clock</code>. Результат — <code>stop-undetermined-time-boundary</code>. Если нужна настоящая длительность, добавьте отдельный контракт с единицами и источником измерения. Не переводите условный такт в миллисекунды задним числом.</p>\n<p>Проверьте также дубликат модели: замените D-участника вторым PHP-участником. Функция вернёт <code>stop-incomplete-adapter-set</code>. Это маленькая деталь, но без неё матрица доказывает лишь три строки данных, а не участие трёх заявленных runtime. Такой отрицательный тест должен жить рядом с положительным и запускаться на каждое изменение схемы.</p>\n<h2>Как встроить проверку в реальный обмен</h2>\n<ol><li>Назовите операцию и владельца её бизнес-смысла. «Три языка совместимы» — слишком широкое утверждение для теста.</li><li>Опишите версию схемы, tag, единицы чисел, обязательные поля, допустимый <code>null</code> и поведение неизвестных полей.</li><li>Разделите результат и ошибку. Для каждого error code укажите, сохранён ли эффект, разрешён ли повтор и кто принимает решение.</li><li>Выберите одну шкалу времени для каждого измерения. Порядковые метки, календарные даты и latency не смешивайте в одном поле.</li><li>Зафиксируйте вход и выход каждого адаптера. Логируйте идентификатор операции, но не подменяйте им доказательство корректности.</li><li>Запустите положительный случай и минимум четыре отрицательных: неполная схема, неизвестный tag, смешанная ошибка и незакрытый интервал.</li><li>Проверьте побочные эффекты отдельно: повторная доставка, транзакция, дедупликация и восстановление после таймаута не следуют из формы JSON.</li><li>Только после этого подключайте transport, конкретные версии runtime и наблюдаемость. Результат локального теста не переносите на production без отдельного измерения.</li></ol>\n<h2>Ограничения применимости</h2>\n<p>Матрица проверяет согласованность выбранного envelope, а не эквивалентность языков. Она не описывает сборщик мусора, правила ABI, сериализатор, планировщик, права, транзакции, порядок доставки сообщений или лимиты сети. Каждое из этих свойств может изменить результат и требует собственной проверки.</p>\n<p>Даже официальный документ языка отвечает на вопрос о языке, а не о вашем API. PHP может привести скаляр до вызова функции; JavaScript может завершить функцию значением или исключением; D использует свою модель ошибок. Между этим поведением и внешним JSON находится ваш код. Именно его контракт и тесты должны объяснить, что увидит соседний участник.</p>\n<p>Учебный пример допускает только три фиксированные модели и одну шкалу времени. В рабочем проекте может быть больше адаптеров, несколько версий схемы или асинхронная доставка. Тогда нужно версионировать набор правил и явно описать совместимость между версиями. Нельзя расширить список участников молча и сохранить старый критерий готовности.</p>\n<p>Наконец, наличие <code>error.code</code> не доказывает, что операция безопасна для повтора. Для этого нужны идемпотентный ключ, граница фиксации эффекта и тест повторной доставки. Если такие условия не помещаются в текущую запись, вывод ограничивается проверкой формы и не распространяется на бизнес-результат.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Граница готова к интеграционному тесту, если инженер без устного пояснения может назвать операцию, версию схемы, допустимый tag, единицы каждого поля, режим ошибки, смысл retry и шкалу времени. Для каждого отрицательного случая заранее известна точная причина остановки.</p>\n<p>Дополнительно должны выполняться три условия: каждый runtime проходит через явный адаптер; преобразования записаны и покрыты тестом; положительный результат не утверждает latency, deployment или успешную транзакцию, если эти свойства отдельно не наблюдались. Если одно поле приходится угадывать, правильный результат проверки — отказ с именем причины, а не зелёная строка для удобства отчёта.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc8259\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format</a> — нормативное описание синтаксиса и типов JSON. Оно не определяет бизнес-смысл полей или политику повторов.</li><li><a href=\"https://262.ecma-international.org/16.0/\" target=\"_blank\" rel=\"noopener noreferrer\">ECMAScript 2025 Language Specification, ECMA-262, 16th edition</a> — официальная спецификация типов языка и completion records JavaScript. Она не задаёт внешний envelope для PHP или D.</li><li><a href=\"https://www.php.net/manual/en/language.types.declarations.php\" target=\"_blank\" rel=\"noopener noreferrer\">PHP Manual: Type declarations</a> — официальное описание проверки объявленных типов, приведения скаляров, strict typing и <code>TypeError</code>. Конкретный boundary зависит от места вызова и версии PHP.</li><li><a href=\"https://dlang.org/spec/errors.html\" target=\"_blank\" rel=\"noopener noreferrer\">D Programming Language Specification: Errors</a> — официальное описание модели ошибок D. Документ не устанавливает общий транспортный контракт с другими runtime.</li></ul>"
|
||
}
|