Как стать автором
Обновить

Как делать бинарно-совместимые API на компилируемых языках

Время на прочтение10 мин
Количество просмотров2.9K
Всего голосов 20: ↑20 и ↓0+23
Комментарии8

Комментарии 8

Обратная совместимость (это когда новое приложение загружает старую библиотеку) вами хорошо описана и самый частый случай и не особо сложная. Расскажите как вы добиваетесь прямой совместимости (это когда старое приложение загружает новую библиотеку). Особо интересен случай когда поддержка прямой совместимости со всеми старыми версиями приложения не целесообразна и нужно остечь/установить минимальную версию приложения с которым библиотека совместима. Конечно версия приложения до этой минимальной не должна крашится при попытке загрузить несовместимую библиотеку, а корректно сообщать что библиотека не совместима. Ещё более интересен случай когда у вас много приложений и у каждого своя версия, а библиотека совместима с разными минимальными версиям каждого приложения. Ещё более интересный случай это когда список приложений открытый т.е. приложения с которыми нужна прямая совместимость написаны не вами и вы о них ничего не знаете. Т.е. вы разработчик плагина/библиотеки, а не приложений.

В норме — за счет того, что старая функциональность библиотеки работает и в старой, и в новой версии библиотеки. Если нужно расширить API несовместимым способом, то появляется новый API рядом. (Но стараемся, конечно, закладывать расширяемость изначально.)

Впрочем, бывают ситуации, когда в определенныех версиях библиотеки что-то было сломано, и приложение должно хендлить эту ситуацию с помощью runtime-проверки. В моем случае, например, была проблема, что библиотека экспортировала не все нужные символы, и нужно было проверять это с помощью dlopen() + dlsym() в рантайме в приложении.

Еще интересный момент, когда старое поведение мы считаем неправильным (например, неочевидным для программиста и ведущим к ошибкам) и хотим поменять несовместимым образом целенаправленно. Это валидное изменения для новой мажорной версии.

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

Мы сделали этот процесс более плавным. Когда несовместимая функциональность появляется, поведение по дефолту не меняется, но новое поведение может быть включено так называемым compat-флагом.

В следующей мажорной версии новое поведение становится дефолтным. Но все еще можно попросить библиотеку (в нашем случае — тарантул) работать по-старому.

А очередная мажорная версия удаляет старое поведение.

Таким образом, можно обновиться, оценить необходимые доработки и делать их постепенно. Новые мажорные версии у нас выходят примерно раз в два года. Соответственно, есть 4 года на обновление кода приложения с возможностью продолжать использовать актуальные версии тарантула.

Я бы еще упомянул, что часто мы сами являемся пользователями своих API (в модулях расширения, в примерах для документации, в решениях для заказчиков), и за счет интеграционного тестирования отлавливаем часть несовместимостей, которые иначе могли бы пройти незамеченными.

Вот да интересен случай когда надо осознано внести несовместимые изменения, но при этом не ломать всех разом, но и когда надо все таки поломать но корректно. В моем случае это не библиотека, а база но суть тоже самое. Я делаю несовместимую миграцию которая исправляет проблемы, но в 2 шага/релиза. На первом шаге я меняю приложение и подготавливаю его для будущей миграции таким образом версия приложения Н+1 может работать и со старой и с новой базой, а версия Н только со старой. Затем в следующем релизе я сделаю миграцию и надо сделать так, что бы версия Н перестала загружать новую базу но не крашилась, а версия Н+1 и новее могла.

Раньше у нас это решалось через проверку мажорной версии базы если она поднимается то старые приложения перестают ее грузить, но очевидно в таком подходе все приложения кроме самой новой версии не смогут работать с новой базой. Я поменял этот механизм и стал в самой базе хранить минимальную совместимую версию приложения и в следующем релизе я проставлю ее как Н+1 и подниму мажорную версию базы. Таким образом Н и более ранние версии перестанут работать, а Н+1 и новее будут работать.

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

В вашей постановке задачи мне видится разумным подход с версионированием схемы базы данных: одна версия на всю базу, на отдельные таблицы, а то и на конкретные записи — зависит от размера таблиц, SLA и необходимости делать миграции «на живую». В этом случае приложения могут иметь список поддерживаемых версий схемы (базы данных, таблицы, записей) и обрабатывать ситуацию совместимой и несовместимой версии.

Здесь мы заменили хранение версии приложения на хранение версии схемы базы плюс (что важно) контракт с приложением о том, как проверяется версия схемы. За счет этого любое приложение, соблюдающее этот контракт, работает ожидаемым для нас образом, и список приложений открытый.

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

Впрочем, я могу не знать какой-то специфики вашего проекта и не возьмусь утверждать, что мои рассуждения применимы к нему.

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

Удобно, когда перед таблицей есть слой логики, где можно заниматься трансформацией данный в зависимости от версии схемы данных и версии API.

Пожалуй, мы относительно далеко уже ушли от темы доклада/статьи :)

Размеры скалярных типов данных и выравнивание. Примечательно, что выравнивание в AMD64 натуральное — по кратности, которая соответствует размеру типа. В x86 это не всегда соблюдалось, что создавало трудности.

ARM64?

Да, натуральное выравнивание. См. раздел 5 (Data types and alignment) стандарта Procedure Call Standard for the Arm 64-bit Architecture. Сборки документа выкладываются здесь: https://github.com/ARM-software/abi-aa/releases

https://flatbuffers.dev/

Flexible - Optional fields means not only do you get great forwards and backwards compatibility (increasingly important for long-lived games: don't have to update all data with each new version!). It also means you have a lot of choice in what data you write and what data you don't, and how you design data structures.

Да, интересный класс zero-copy форматов сериализации: я бы еще обратил внимание на Cap'n Proto от одного из авторов Protocol Buffers.

Сравнение от автора Cap'n Proto: https://capnproto.org/news/2014-06-17-capnproto-flatbuffers-sbe.html

Зарегистрируйтесь на Хабре, чтобы оставить комментарий