Files
progcode/editorial/agent-rewrites/231.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 231,
"slug": "editorial-2021-08-practice-storage-contracts",
"title": "Контракт хранилища: как менять profile/settings без поломки старых читателей",
"excerpt": "JSON задаёт форму, но не объясняет смысл поля, различие между absent и null и границы совместимости. Разбираем контракт profile/settings, матрицу writer/reader и безопасный порядок изменения.",
"contentHtml": "<p>Сбой начинается после обычного релиза. Новый writer добавляет <code>timezone</code> в <code>profile/settings</code>. Старый reader получает запись и либо падает на неизвестном поле, либо принимает отсутствие <code>timezone</code> за явную очистку. Пользователь видит сброшенную настройку или ошибку загрузки профиля. Команда тратит время на восстановление уже записанных данных.</p>\n<p>Цена ошибки выше, чем один неудачный запрос. Новая версия пишет данные, которые ещё не умеют читать все потребители. Если поле уже меняет смысл, откат кода не возвращает старый смысл автоматически. Поэтому формат записи нельзя считать полным контрактом. Нужны правила владения, presence, типов, значений, версий и отката.</p>\n<h2>Тезис: хранилище обязано сохранять договор, а не только байты</h2>\n<p>В этой статье договор описывает запись <code>profile.settings</code>. Он не зависит от конкретной базы, файла или очереди. Объект имеет стабильный идентификатор <code>profile.id</code>. Поле <code>settings.emailDigest</code> хранит режим частоты сводки: <code>off</code>, <code>weekly</code> или <code>daily</code>. Новое поле <code>timezone</code> необязательно. Для него различаются три состояния: ключ отсутствует, ключ содержит <code>null</code>, ключ содержит непустую строку.</p>\n<p>Это учебная модель. Она работает на объектах в памяти и показывает границу между writer и reader. Она не проверяет выбранную БД, репликацию, миграционный job, сеть или реальный rollout. Такие проверки появляются только после привязки правил к конкретной системе.</p>\n<h2>Что входит в контракт</h2>\n<p>Сначала назовите владельца записи. Владелец отвечает за смысл полей и список поддерживаемых потребителей. Затем запишите обязательные ключи, допустимые типы и наборы значений. Отдельной строкой опишите отсутствие ключа и <code>null</code>. Наконец, перечислите пары версий, которые могут работать одновременно.</p>\n<pre><code>const profileSettingsContract = {\n owner: 'profile-settings',\n identity: 'profile.id',\n required: ['id', 'displayName', 'settings.emailDigest'],\n optional: { timezone: 'absent | null | non-empty string' },\n values: { emailDigest: ['off', 'weekly', 'daily'] },\n compatibility: [\n 'writer-v1 -> reader-v2',\n 'writer-v2 -> reader-v1',\n 'writer-v2 -> reader-v2'\n ]\n};</code></pre>\n<p>Поле типа «строка» всё ещё может нарушать договор. Значение <code>weekly</code> должно сохранять смысл режима сводки. Если тот же текст начинают использовать как метку маркетингового сегмента, структурный валидатор не заметит проблему. Это semantic break: тип прежний, а поведение потребителя изменилось.</p>\n<div class=\"table-scroll\"><table><caption>Минимальная карточка договора profile.settings</caption><thead><tr><th scope=\"col\">Часть</th><th scope=\"col\">Правило</th><th scope=\"col\">Проверка</th><th scope=\"col\">Граница</th></tr></thead><tbody><tr><td><code>id</code></td><td>Непустая строка, одна запись профиля</td><td>Проверить тип и связь с profile</td><td>Не доказывает существование профиля в БД</td></tr><tr><td><code>emailDigest</code></td><td><code>off</code>, <code>weekly</code> или <code>daily</code></td><td>Проверить множество значений</td><td>Строка сама не раскрывает смысл</td></tr><tr><td><code>timezone</code></td><td>Absent, <code>null</code> или непустая строка</td><td>Проверить наличие собственного ключа</td><td>Нельзя без правила заменить absent на <code>null</code></td></tr><tr><td>Версии</td><td>Старые и новые пары читают поддерживаемые записи</td><td>Прогнать матрицу samples</td><td>Не покрывает неизвестного consumer</td></tr></tbody></table></div>\n<h2>Почему absent не равно null</h2>\n<p>Старый writer, который не знает о <code>timezone</code>, не передаёт этот ключ. Такое состояние сообщает только об отсутствии данных в версии writer. Оно не означает, что пользователь очистил часовой пояс. Явное <code>null</code> может означать очистку, если именно это установил владелец контракта. Значение-строка означает заданный часовой пояс. Пустая строка запрещена: для неё нет определённого смысла.</p>\n<pre><code>const hasOwn = (object, key) =&gt;\n Object.prototype.hasOwnProperty.call(object, key);\n\nfunction readTimezone(record) {\n if (!hasOwn(record, 'timezone')) return { state: 'absent' };\n if (record.timezone === null) return { state: 'explicit-null' };\n if (typeof record.timezone === 'string' &amp;&amp; record.timezone.length &gt; 0) {\n return { state: 'value', value: record.timezone };\n }\n throw new Error('timezone violates profile/settings contract');\n}\n\nreadTimezone({ id: 'profile-17' });\nreadTimezone({ id: 'profile-17', timezone: null });</code></pre>\n<p>Проверка <code>if (record.timezone)</code> здесь ошибочна. Она смешивает отсутствие ключа, <code>null</code> и пустую строку. Проверка собственного ключа сохраняет наблюдаемое состояние. Reader должен либо вернуть известный результат, либо остановить интерпретацию. Молчаливый fallback прячет нарушение и записывает неверное решение в следующий слой.</p>\n<figure><img src=\"/assets/editorial/2021/storage-contract-compatibility-2021.svg\" alt=\"Матрица совместимости profile.settings: writer v1 и v2, reader v1 и v2; v2 добавляет timezone, reader v2 различает absent, null и значение\" loading=\"lazy\" /><figcaption>Дополнительное поле безопасно только для проверенной пары writer и reader. Схема иллюстрирует правила статьи, а не гарантии конкретного хранилища.</figcaption></figure>\n<h2>Совместимость проверяют в обе стороны</h2>\n<p>Пусть writer v1 пишет только обязательные поля. Writer v2 добавляет <code>timezone</code>, не меняя существующие значения. Reader v1 читает известные обязательные поля и игнорирует неизвестное необязательное поле. Reader v2 понимает старую запись, возвращает для неё состояние <code>absent</code> и умеет обработать значение и явный <code>null</code>.</p>\n<pre><code>const compatibility = [\n ['writer v1', 'reader v1', 'accept'],\n ['writer v1', 'reader v2', 'accept: timezone absent'],\n ['writer v2 additive', 'reader v1', 'accept: unknown optional'],\n ['writer v2', 'reader v2', 'accept: value and explicit null'],\n ['writer v1 daily', 'narrowed reader', 'reject before release']\n];</code></pre>\n<p>Неизвестное поле можно игнорировать только тогда, когда оно не влияет на старое обязательное поведение. Если новый writer добавил <code>timezone</code>, а старый reader теперь должен изменить расчёт уведомлений, изменение уже не additive. Если новый reader перестал принимать старое <code>daily</code>, он сузил множество допустимых значений. Обе ситуации требуют остановки выпуска и отдельного переходного договора.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика разрыва контракта</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Профиль не читается после записи</td><td>Reader отвергает новое поле или тип</td><td>Сравнить record, версии и путь ошибки</td><td>Добавить reader, который понимает старый record; writer не включать раньше него</td></tr><tr><td>Часовой пояс сбросился</td><td>Absent трактовали как явный clear</td><td>Проверить наличие собственного ключа и исходный writer</td><td>Развести ветви absent и <code>null</code>, добавить samples обоих состояний</td></tr><tr><td>Старая сводка перестала работать</td><td>Сузили допустимые значения или сменили смысл</td><td>Прогнать все старые значения через новый reader</td><td>Отклонить narrowing или выпустить отдельный перевод смысла</td></tr><tr><td>После отката остаются неверные настройки</td><td>Writer уже записал новый смысл</td><td>Найти записи новой версии и сравнить семантику</td><td>Остановить producer, сохранить samples и планировать контролируемое преобразование</td></tr></tbody></table></div>\n<h2>Порядок изменения</h2>\n<ol><li>Зафиксируйте текущий договор: owner, обязательные поля, допустимые значения и смысл каждого значения.</li><li>Соберите несколько обезличенных старых records. Для каждого укажите writer и ожидаемый результат чтения.</li><li>Разделите новые состояния. Для <code>timezone</code> отдельно опишите absent, <code>null</code> и строку.</li><li>Проверьте новый reader на старых records. Он не должен превращать отсутствие нового ключа в другое состояние.</li><li>Добавьте новый writer только после проверки старого reader. Новое поле должно быть необязательным для старого пути.</li><li>Проверьте новую пару и отрицательные случаи: неправильный тип, пустую строку, narrowing и смену смысла.</li><li>Перед удалением legacy-пути подтвердите, что поддерживаемых старых consumers и records больше нет. Одной новой версии схемы для этого недостаточно.</li></ol>\n<h2>Отрицательный путь и откат</h2>\n<p>Удачная запись не доказывает совместимость. Нужны примеры, которые должны быть отклонены. <code>timezone: 42</code> нарушает тип. Пустая строка нарушает ограничение значения. Reader, который принимает только <code>off</code> и <code>weekly</code>, нарушает совместимость со старым writer, если тот законно писал <code>daily</code>. Reader, который читает <code>weekly</code> как маркетинговую метку, нарушает смысл.</p>\n<p>При additive-изменении откат обычно ограничивается остановкой нового writer: reader уже понимает старые и новые записи. При semantic change такой откат недостаточен. Старые байты уже несут новое значение. Сначала остановите producer, зафиксируйте затронутые records и определите обратимое преобразование. Учебный код статьи не выполняет такое преобразование и не подтверждает, что оно безопасно для конкретной системы.</p>\n<h2>Ограничения модели</h2>\n<p>JSON не описывает владельца поля, срок поддержки версии или бизнес-смысл строки. Валидатор схемы может проверить структуру, тип и часть ограничений, но не узнает, что <code>weekly</code> нельзя переиспользовать. Не каждый reader должен игнорировать неизвестные поля: это решение зависит от критичности данных и правил формата. В некоторых системах безопаснее отклонить запись, чем потерять неизвестное обязательное поведение.</p>\n<p>Порядок ключей объекта не используется как сигнал версии. Reader обращается к именам, а не к позиции. Если системе нужен канонический текст для подписи или хеша, это отдельный договор сериализации. Статья также не утверждает наличие SLA, метрик, успешной миграции или production-результата: приведённые записи и проверки учебные.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Изменение готово к выпуску, если владелец может назвать поддерживаемые пары writer/reader, новый reader проходит все старые samples, старый reader не зависит от нового optional поля, а отрицательные samples останавливают type break, narrowing и semantic break. Для <code>timezone</code> проверка должна различать absent, явный <code>null</code> и непустую строку. Для отката должен существовать понятный стоп-сигнал producer и способ обнаружить уже записанные новые значения.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.rfc-editor.org/rfc/rfc8259.html\" target=\"_blank\" rel=\"noopener noreferrer\">RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format</a> — официальный стандарт IETF описывает object как набор пар имя/значение и предупреждает о зависимости от порядка членов.</li><li><a href=\"https://json-schema.org/draft/2020-12/json-schema-validation\" target=\"_blank\" rel=\"noopener noreferrer\">JSON Schema 2020-12: Validation vocabulary</a> — официальная спецификация языка структурных assertions, включая тип <code>null</code>; семантика приложения остаётся отдельным правилом.</li></ul>"
}