тут конечно от адаптера все зависит. лучше всего это поддерживает в compact: там типы записываются так что любое поле может быть пропущено. то есть любую структуру можно прочитать, не знаю куда она должна быть декодирована, будь то простой тип, вектор, структура, вложенная древовидная структура. не нашли куда ее расспокавать по ID и ее просто скипнут из бинарной записи, и данные не будут испорчены, следующие данные будут прочитаны. Но тут вопрос к кодекам, они не должны вернуть ошибку. как таковая схема не нужна, текущая схема у тебя уже в коде.
типы пишутся вместе с данными как wire_type, но тут не имена типов конечно. и пишутся размеры которые облегчают декодирование.
или у тебя вопрос а можно ли схему данных получить динамически и сохранить как либы протобафа делают при генерации? тут ответ да, но пока не сделано, но это достаточно просто, к примеру кодек дебаг адаптера почти все это делает и даже больше. в планах есть генерация схемы.
на самом деле новый класс нужен далеко не всегда, это самый сложный путь, он нужен когда тебе нужно проводить major миграцию схемы по сути, давай на примерах: когда новый класс точно не нужен.
ты решил добавить новое поле bool paid; ты решаешь какое значение по умолчанию к него будет paid = false; и записываешь его в контракт (paid, 4). и этого хватит, схема поменялось но все может работать.
если старое приложение получило новые пакет, оно прочитает ID 4, не найдет его у себя в структуре и при желании сможет пропустить (так сейчакс делает compact адаптер, а протобаф можно просто научить).
если новое приложение читает старые данные из бекапа, базы, импорта: оно не найдет ID 4 и останется значение по умолчанию.
это поведение и есть ленивая миграция схемы.
но она требует понимания что контракт данных сможет так работать, там бывает часто, но не всегда. а вот если это не подходит, как раз нужно делать Class_v1, Class_v2, Class_v3, а это отдельное боль и страдание. когда это точно нужно… когда ты поменял типы так что их даже нельзя автосконвертить через кодеки, например е тебя был int, а ты решил там хранить vector, и нужно явная функция конверта v1 - v2.
в целом думаю через CONTRACT это на уровне схемы описать реально, и даже функуции конверта можно сделать удобные. я подумаю) это очень похоже на наследование BASE(Header, 100). типа:
CONTRACT (Event_v2, #2
OLD(Event_v1)
)
ну и функцию конверта придется явно писать на все дерево.
адаптер действительно универсален, его менять не нужно, ты просто используешь его. ну и клиллер фича еще в том что ты пишешь одну схему и работают все поддерживаемые адаптеры. ты можешь данные передавать в сеть по протобафу, выводить в лог в json, выводить в дебаг на экран, писать compact в файл. и это просто удобно)
по поводу совместимости типов, тут уже дело адаптера конкретного, тот же протобаф и compact целые числа сжимают и любое целое можно распаковать в другое. с плавающей точкой сложнее, там это разные типы и каста нет. но при желании это можно сделать в кодеках типов адаптеров. BCD примерно тоже самое тоже нужно делать поддержку конверта в codec<T> - они в целом для этого и вынесены отдельно.
а вот если добавилось новое поле или удалилось, то все проблемы решаются легко за счет ID в схеме, как я писал выше.
на практике этого всегда почти хватало: типы полей меняются не часто, самый частый кейс int закрывается. расширение схемы работает, при получении старым клиентом новой схемы при некоторой осторожности все будет работать: новые поля, которых еще нет, пропустятся, старые которые удалили, получат дефолтные значения. и все будет работать.
такая же схема когда данные сохранил в файл, и через год читаешь. что возможно прочитаешь. мы такую схему использовали с compact и сохраняли стейт приложений для бекапа или рестарта между версиями, это работало. Единственно ограничение когда у тебя схема меняется настолько что нужно писать конвертер от v1 к v2 потому что у тебя все поменялось и ни о какой совместимости речи уже на уровне схемы не идет и автоматически тут ничего не сделать. можно подумать над слоем версионности структур, но это усложнить CONTRACT и вводить в типы версии, и функции конверта... нада подумать.
все твои пожелания можно закрыть через приведение типов в конкретных адаптерах в codec<T>. Думаю даже это не сильно снизит скорость работы, hot path можно сделать на совпадения типа, а попытку каста на fallback.
если интересно попробовать, опиши где нужна лучшая совместимость, там очень небольшие фиксы)
это сигнатура кодека: codec<T>::read(Reader&, Field&, wire_type wire, T& value)
На уровне схемы это дополняется словарём reserved_id/reserved_range/deprecated/alias — можно явно объявить, что конкретный id зарезервирован или устарел, прямо в контракте, а не в комментарии рядом с полем. Сегодня это декларативная информация: ни один адаптер её не проверяет в рантайме, но история схемы уже фиксируется в одном месте.
Зависит от того, что именно понимать под версионностью.
Сам CONTRACT задаёт основу для эволюции схемы: у полей есть стабильные ID, а формат и правила чтения принадлежат адаптеру. Поэтому конкретный адаптер может реализовать нужную стратегию — пропуск неизвестных полей, сохранение default-значений для отсутствующих, строгую проверку схемы или явные преобразования.
В compact базовая эволюция схемы уже реализована: неизвестные поля пропускаются, а при отсутствии известных полей сохраняются их текущие или default-значения. Поэтому поля можно добавлять, удалять и переименовывать без изменения ID — старые и новые сообщения остаются читаемыми.
Protobuf-адаптер сейчас намеренно строгий: неизвестное поле считается ошибкой. Для совместимости при развитии схемы reader должен пропускать неизвестные значения с учётом их wire type. Это локальное и вполне реализуемое расширение, которое логично оформить как настройку адаптера. Буду благодарен за issue, особенно с описанием вашего сценария.
Полноценной системы миграций v1 → v2 → v3 для несовместимых изменений типов или семантики сейчас действительно нет. Это отдельный слой. Поэтому интересно, что требуется в вашем случае: rolling upgrade сервисов или чтение объектов, сохранённых несколько лет назад?
Собеседования безусловно поменялись, но я их раньше проводил методом решения сложной задачи, на словах, в диалоге, оценивал понимание задачи, умение схватывать на лету идеи или новую сложную информацию. Что часто бывает в рабочей задачи и показывает именно инженерную работу. И как оказалась работа с нейронками - это тот же самый формат)) и ребята слабее стали лучше проходить собесы, думал что такое... а их неплохо натаскивает аи на такие задачи и в целом это не плохо... их скилл то растет)
тут конечно от адаптера все зависит.
лучше всего это поддерживает в compact: там типы записываются так что любое поле может быть пропущено. то есть любую структуру можно прочитать, не знаю куда она должна быть декодирована, будь то простой тип, вектор, структура, вложенная древовидная структура. не нашли куда ее расспокавать по ID и ее просто скипнут из бинарной записи, и данные не будут испорчены, следующие данные будут прочитаны. Но тут вопрос к кодекам, они не должны вернуть ошибку. как таковая схема не нужна, текущая схема у тебя уже в коде.
типы пишутся вместе с данными как wire_type, но тут не имена типов конечно. и пишутся размеры которые облегчают декодирование.
или у тебя вопрос а можно ли схему данных получить динамически и сохранить как либы протобафа делают при генерации? тут ответ да, но пока не сделано, но это достаточно просто, к примеру кодек дебаг адаптера почти все это делает и даже больше. в планах есть генерация схемы.
PaymentConfig:service: "payment" # #1 std::stringport: 8080 # #2 u32enabled: true # #3 booltags: # #4 std::vector<std::string>, size=3- "api" # [0]- "payments" # [1]- "production" # [2]на самом деле новый класс нужен далеко не всегда, это самый сложный путь, он нужен когда тебе нужно проводить major миграцию схемы по сути, давай на примерах: когда новый класс точно не нужен.
давай на примерах:
struct Order {
std::uint64_t id;
std::string customer;
double amount;
CONTRACT(Order, (id, 1), (customer, 2), (amount, 3) )
};
ты решил добавить новое поле bool paid; ты решаешь какое значение по умолчанию к него будет paid = false; и записываешь его в контракт (paid, 4). и этого хватит, схема поменялось но все может работать.
если старое приложение получило новые пакет, оно прочитает ID 4, не найдет его у себя в структуре и при желании сможет пропустить (так сейчакс делает compact адаптер, а протобаф можно просто научить).
если новое приложение читает старые данные из бекапа, базы, импорта: оно не найдет ID 4 и останется значение по умолчанию.
это поведение и есть ленивая миграция схемы.
но она требует понимания что контракт данных сможет так работать, там бывает часто, но не всегда. а вот если это не подходит, как раз нужно делать Class_v1, Class_v2, Class_v3, а это отдельное боль и страдание. когда это точно нужно… когда ты поменял типы так что их даже нельзя автосконвертить через кодеки, например е тебя был int, а ты решил там хранить vector, и нужно явная функция конверта v1 - v2.
в целом думаю через CONTRACT это на уровне схемы описать реально, и даже функуции конверта можно сделать удобные. я подумаю) это очень похоже на наследование BASE(Header, 100). типа:
CONTRACT (Event_v2, #2
OLD(Event_v1)
)
ну и функцию конверта придется явно писать на все дерево.
адаптер действительно универсален, его менять не нужно, ты просто используешь его. ну и клиллер фича еще в том что ты пишешь одну схему и работают все поддерживаемые адаптеры. ты можешь данные передавать в сеть по протобафу, выводить в лог в json, выводить в дебаг на экран, писать compact в файл. и это просто удобно)
по поводу совместимости типов, тут уже дело адаптера конкретного, тот же протобаф и compact целые числа сжимают и любое целое можно распаковать в другое. с плавающей точкой сложнее, там это разные типы и каста нет. но при желании это можно сделать в кодеках типов адаптеров. BCD примерно тоже самое тоже нужно делать поддержку конверта в codec<T> - они в целом для этого и вынесены отдельно.
а вот если добавилось новое поле или удалилось, то все проблемы решаются легко за счет ID в схеме, как я писал выше.
на практике этого всегда почти хватало: типы полей меняются не часто, самый частый кейс int закрывается.
расширение схемы работает, при получении старым клиентом новой схемы при некоторой осторожности все будет работать: новые поля, которых еще нет, пропустятся, старые которые удалили, получат дефолтные значения. и все будет работать.
такая же схема когда данные сохранил в файл, и через год читаешь. что возможно прочитаешь. мы такую схему использовали с
compactи сохраняли стейт приложений для бекапа или рестарта между версиями, это работало. Единственно ограничение когда у тебя схема меняется настолько что нужно писать конвертер от v1 к v2 потому что у тебя все поменялось и ни о какой совместимости речи уже на уровне схемы не идет и автоматически тут ничего не сделать. можно подумать над слоем версионности структур, но это усложнить CONTRACT и вводить в типы версии, и функции конверта... нада подумать.все твои пожелания можно закрыть через приведение типов в конкретных адаптерах в codec<T>. Думаю даже это не сильно снизит скорость работы, hot path можно сделать на совпадения типа, а попытку каста на fallback.
если интересно попробовать, опиши где нужна лучшая совместимость, там очень небольшие фиксы)
это сигнатура кодека:
codec<T>::read(Reader&, Field&, wire_type wire, T& value)в ее придется добавить ветку на fallback
if (wire == detail::wire_type::fixed64) {На уровне схемы это дополняется словарём
reserved_id/reserved_range/deprecated/alias— можно явно объявить, что конкретный id зарезервирован или устарел, прямо в контракте, а не в комментарии рядом с полем. Сегодня это декларативная информация: ни один адаптер её не проверяет в рантайме, но история схемы уже фиксируется в одном месте.Зависит от того, что именно понимать под версионностью.
Сам CONTRACT задаёт основу для эволюции схемы: у полей есть стабильные ID, а формат и правила чтения принадлежат адаптеру. Поэтому конкретный адаптер может реализовать нужную стратегию — пропуск неизвестных полей, сохранение default-значений для отсутствующих, строгую проверку схемы или явные преобразования.
В
compactбазовая эволюция схемы уже реализована: неизвестные поля пропускаются, а при отсутствии известных полей сохраняются их текущие или default-значения. Поэтому поля можно добавлять, удалять и переименовывать без изменения ID — старые и новые сообщения остаются читаемыми.Protobuf-адаптер сейчас намеренно строгий: неизвестное поле считается ошибкой. Для совместимости при развитии схемы reader должен пропускать неизвестные значения с учётом их wire type. Это локальное и вполне реализуемое расширение, которое логично оформить как настройку адаптера. Буду благодарен за issue, особенно с описанием вашего сценария.
Полноценной системы миграций
v1 → v2 → v3для несовместимых изменений типов или семантики сейчас действительно нет. Это отдельный слой. Поэтому интересно, что требуется в вашем случае: rolling upgrade сервисов или чтение объектов, сохранённых несколько лет назад?Собеседования безусловно поменялись, но я их раньше проводил методом решения сложной задачи, на словах, в диалоге, оценивал понимание задачи, умение схватывать на лету идеи или новую сложную информацию. Что часто бывает в рабочей задачи и показывает именно инженерную работу. И как оказалась работа с нейронками - это тот же самый формат)) и ребята слабее стали лучше проходить собесы, думал что такое... а их неплохо натаскивает аи на такие задачи и в целом это не плохо... их скилл то растет)