Интересный кейс от Uber с реализацией механизма обработки ошибок запросов, во многом похожий на паттерн Circuit Breaker.
Отличие от CB в том, что основная цель не просто временно прекратить проблемные запросы, а продолжить отправлять запросы на лучший из доступных доменов.
Ряд доменов у Uber нацелены на прокси сервера в из облачной инфраструктуре, которая распределена по разным регионам, основным доменом для приложения считается ближайший, в запасных есть и другие прокси домены, и домен, который смотрит напрямую в дата-центр Uber.
Вся edge инфраструктура дает профит в скорости установления TLS соединения, меньше задержек и ошибок у HTTPS трафика, плюс так как это обычно популярные cloud сервисы, на них могут использоваться современные протоколы (привет, QUIC!)
В статье рассказано про то, как сложно понять что произошла именно ошибка связанная с проблемами доступа к хосту, что-то уровня DNS / TCP, таймауты запросов, но не такие ошибки, как отсутствие связи в тоннеле, или ошибки непосредственно от API.
Основная часть статьи - описание механизма, который отвечает за то, что бы запросы всегда уходили на самый подходящий хост, но умели использовать запасные, плюс различные графики, измеряющие профит.
Не буду все подробно переводить, но на верхнем уровне, этот механизм - симпатичная стейт-машина, где все состояния стремятся к основному, когда используется лучший домен, т.е. система стабильна.
Для проверки доменов отправляются запросы на health эндпоинте.
https://eng.uber.com/eng-failover-handling/
Отличие от CB в том, что основная цель не просто временно прекратить проблемные запросы, а продолжить отправлять запросы на лучший из доступных доменов.
Ряд доменов у Uber нацелены на прокси сервера в из облачной инфраструктуре, которая распределена по разным регионам, основным доменом для приложения считается ближайший, в запасных есть и другие прокси домены, и домен, который смотрит напрямую в дата-центр Uber.
Вся edge инфраструктура дает профит в скорости установления TLS соединения, меньше задержек и ошибок у HTTPS трафика, плюс так как это обычно популярные cloud сервисы, на них могут использоваться современные протоколы (привет, QUIC!)
В статье рассказано про то, как сложно понять что произошла именно ошибка связанная с проблемами доступа к хосту, что-то уровня DNS / TCP, таймауты запросов, но не такие ошибки, как отсутствие связи в тоннеле, или ошибки непосредственно от API.
Основная часть статьи - описание механизма, который отвечает за то, что бы запросы всегда уходили на самый подходящий хост, но умели использовать запасные, плюс различные графики, измеряющие профит.
Не буду все подробно переводить, но на верхнем уровне, этот механизм - симпатичная стейт-машина, где все состояния стремятся к основному, когда используется лучший домен, т.е. система стабильна.
Для проверки доменов отправляются запросы на health эндпоинте.
https://eng.uber.com/eng-failover-handling/
Про паттерн Circuit Breaker можно почитать у Мартина Фаулера - https://martinfowler.com/bliki/CircuitBreaker.html
Мы в ряде фронтовых приложений Тинькофф используем CB уровня приложения, реализацию можно посмотреть на гитхабе - https://github.com/TinkoffCreditSystems/tinkoff-request/blob/master/packages/plugin-circuit-breaker/src/CircuitBreaker.ts
Можно считать это продвинутым механизмом таймаутов, который позволяет при проблемах с API незамедлительно на проблемные запросы отдавать ответы из кэша, либо последнюю ошибку, не дожидаясь таймаутов.
Дополнительно, это облегчает нагрузку на проблемный API
Мы в ряде фронтовых приложений Тинькофф используем CB уровня приложения, реализацию можно посмотреть на гитхабе - https://github.com/TinkoffCreditSystems/tinkoff-request/blob/master/packages/plugin-circuit-breaker/src/CircuitBreaker.ts
Можно считать это продвинутым механизмом таймаутов, который позволяет при проблемах с API незамедлительно на проблемные запросы отдавать ответы из кэша, либо последнюю ошибку, не дожидаясь таймаутов.
Дополнительно, это облегчает нагрузку на проблемный API
martinfowler.com
bliki: Circuit Breaker
You use software circuit breakers on connections to remote services. These breakers trip when the supplier becomes unresponsive, once tripped the breaker no longer calls the supplier until reset.
Отдельно ряд ссылочке про протокол QUIC - его активное распространение просто невероятно вдохновляет!
Реализация и успешное внедрение QUIC протокола от Facebook - https://engineering.fb.com/2020/10/21/networking-traffic/how-facebook-is-bringing-quic-to-billions/
Обзорная статья про QUIC от Cloudfire, внутри есть много ссылок с подробной информацией - https://blog.cloudflare.com/last-call-for-quic/
Кстати, рекомендую блоги инженеров Cloudfire и Fastly, много интересного контента.
https://www.fastly.com/blog
Реализация и успешное внедрение QUIC протокола от Facebook - https://engineering.fb.com/2020/10/21/networking-traffic/how-facebook-is-bringing-quic-to-billions/
Обзорная статья про QUIC от Cloudfire, внутри есть много ссылок с подробной информацией - https://blog.cloudflare.com/last-call-for-quic/
Кстати, рекомендую блоги инженеров Cloudfire и Fastly, много интересного контента.
https://www.fastly.com/blog
Engineering at Meta
How Facebook is bringing QUIC to billions
We are replacing the de facto protocol the internet has used for decades with QUIC, the latest and most radical step we’ve taken to optimize our network protocols to create a better experience for …
К разговору об отказоустойчивости, хочу поделиться частным случаем, как повысить надежность загрузки ресурсов фронтовых приложений.
Рецепт узнал у коллег в Тинькофф, также используем в продакшене.
Допустим, у нас есть несколько CDN для загрузки ресурсов - основной, и дополнительный.
Ошибку загрузки
Основная сложность в том, что бы запросить упавший ресурс с дополнительного CDN - это необходимость делать повторный запрос синхронно - например, повторная попытка загрузить
Оказывается, что обработчик
Таким образом, остается только синхронно повторно загрузить упавший ресурс.
На помощь приходит старый товарищ
Минимальный пример кода:
В этот код можно добавить health check запросы к CDN, добавить подробностей в ошибки, например информацию из
Статья в блоге Fastly про NEL - https://www.fastly.com/blog/network-error-logging
Демка на github с повторными запросами:
https://superoleg39.github.io/retry-resources/
https://github.com/SuperOleg39/retry-resources
Рецепт узнал у коллег в Тинькофф, также используем в продакшене.
Допустим, у нас есть несколько CDN для загрузки ресурсов - основной, и дополнительный.
Ошибку загрузки
css или js ресурса можно отловить с помощью обработчика window.onerror.Основная сложность в том, что бы запросить упавший ресурс с дополнительного CDN - это необходимость делать повторный запрос синхронно - например, повторная попытка загрузить
vendor скрипт, содержищий React, должен завершиться до выполнения остальных скриптов приложения.Оказывается, что обработчик
window.onerror выполняется синхронно, и блокирует выполнение JavaScript кода.Таким образом, остается только синхронно повторно загрузить упавший ресурс.
На помощь приходит старый товарищ
XMLHttpRequest, который позволяет нам загрузить код скрипта с fallback CDN, и вставить его на страницу как inline скрипт.Минимальный пример кода:
// этот inline скрипт должен быть расположен перед всеми загружаемыми скриптами и стилями
const primaryDomain = 'https://cdn-first.com/';
const fallbackDomain = 'https://cdn-second.com/';
const retriedResourcesList = {};
const maxRetries = 1;
function retry(event) {
// тут будет ссылка на DOM элемент noscript или link
const tag = event.target;
// если упал файл стилей, получим `LINK` в `tagName`, если скрипт - то `SCRIPT`
const tagName = tag.tagName && tag.tagName.toLowerCase();
if (tagName !== 'link' && tagName !== 'noscript') {
return;
}
const isLink = tagName === 'link';
const failedUrl = isLink ? tag.href : tag.src;
const fallbackUrl = getFallbackUrl(failedUrl);
// простой вариант не уйти в цикл повторных запросов
if (!retriedResourcesList[fallbackUrl]) {
retriedResourcesList[fallbackUrl] = 1;
} else if (retriedResourcesList[fallbackUrl] < maxRetries) {
retriedResourcesList[fallbackUrl] = retriedResourcesList[fallbackUrl] + 1;
} else {
return;
}
const newTag = isLink ? createLink(tag, fallbackUrl) : createScript(tag, fallbackUrl);
// вставляем новый тег рядом с упавшим ресурсом
tag.parentNode.insertBefore(newTag, tag);
}
function getFallbackUrl(url) {
if (url.indexOf(primaryDomain) === 0) {
return url.replace(primaryDomain, fallbackDomain);
}
return url;
}
function createLink(tag, fallbackUrl) {
const newTag = document.createElement('link');
newTag.href = fallbackUrl;
newTag.rel = 'stylesheet';
return newTag;
}
function createScript(tag, fallbackUrl) {
const newTag = document.createElement('noscript');
// тут можно обработать и "обогатить" ошибку
const xhr = new XMLHttpRequest();
xhr.open('GET', fallbackUrl, false);
xhr.send();
newTag.text = xhr.responseText;
return newTag;
}
// пока не выполнится обработчик retry, выполнение JS будет заблокировано!
window.addEventListener('error', retry, true);
В этот код можно добавить health check запросы к CDN, добавить подробностей в ошибки, например информацию из
window.navigator.connection
Отдельно порекомендую механизм NEL - Network Error Logging, который поддерживается в Chromium браузерах, и позволяет отправить невероятно подробные отчеты о сетевых ошибках.Статья в блоге Fastly про NEL - https://www.fastly.com/blog/network-error-logging
Демка на github с повторными запросами:
https://superoleg39.github.io/retry-resources/
https://github.com/SuperOleg39/retry-resources
Fastly
Network error logging: collecting failure conditions from end users | Fastly
In this post, we’ll explore the NEL framework, how it provides visibility, and ways to collect and process the resulting data.
Привет!
Рецепт ретраев загрузки статических ресурсов будет не полным, если не рассмотреть альтернативы.
На самом деле, самый простой и логичный механизм для этого - Service Worker.
Мы можем перехватить ошибку любого запроса, убедиться что это был запрос на статику на наш CDN, и продолжить цепочку промисов запросом на тот же ресурс с другого CDN.
Код в воркере может выглядеть примерно так:
Service Worker позволяет сделать нам гораздо более гибкие стратегии для повторных запросов, сам процесс ретраев будет полностью прозрачным.
Есть и минусы такого подхода:
- SW может быть еще не зарегистрирован, т.е. для первых пользователей сайта ретраи будут не доступны
- Если на домене доступны несколько приложений, но общий Service Worker, в теории эти приложения могут использовать разные CDN, и будет чуть труднее сделать логику ретраев подходящей для всех
- Используется
Но даже с учетом этих нюансов, использование Service Worker кажется более перспективным решением для механизма ретраев.
Альтернативный вариант реализации ретраев - интеграция этого механизма в Webpack.
Мне не нравится тем, что вариант самый не прозрачный и не очевидный, но возможно это дело вкуса)
Как я понимаю, основной вариант реализации ретраев - это расширение возможностей методов webpack_require , например плагин webpack-retry-chunk-load-plugin расширяет webpack_require.e (это require.ensure, который используется для динамических импортов), и в обработчике ошибок загрузки чанка продолжает цепочку промисов повторной загрузкой.
Ссылка на плагин - https://github.com/mattlewis92/webpack-retry-chunk-load-plugin
Никогда не помешает ссылка на великолепный инструмент workbox - https://developers.google.com/web/tools/workbox
Давно не заходил к ним на сайт, появился раздел с рецептами, в том числе пример offline fallback - несколько лет назад мне очень не хватало этого рецепта) https://developers.google.com/web/tools/workbox/modules/workbox-recipes?hl=en#offline_fallback
Рецепт ретраев загрузки статических ресурсов будет не полным, если не рассмотреть альтернативы.
На самом деле, самый простой и логичный механизм для этого - Service Worker.
Мы можем перехватить ошибку любого запроса, убедиться что это был запрос на статику на наш CDN, и продолжить цепочку промисов запросом на тот же ресурс с другого CDN.
Код в воркере может выглядеть примерно так:
self.addEventListener('fetch', event => {
return fetch(event.request).catch((error) => {
const requestToCDN = event.request.url.startsWith(primaryCDN);
const requestToAsset = event.request.url.endsWith('.css') || event.request.url.endsWith('.js'));
if (requestToCDN && requestToAsset) {
const nextUrl = event.request.url.replace(primaryCDN, fallbackCDN);
return fetch(new Request(nextUrl, event.request));
}
throw error;
});
});
Service Worker позволяет сделать нам гораздо более гибкие стратегии для повторных запросов, сам процесс ретраев будет полностью прозрачным.
Есть и минусы такого подхода:
- SW может быть еще не зарегистрирован, т.е. для первых пользователей сайта ретраи будут не доступны
- Если на домене доступны несколько приложений, но общий Service Worker, в теории эти приложения могут использовать разные CDN, и будет чуть труднее сделать логику ретраев подходящей для всех
- Используется
workbox , и вклиниться в существующий процесс кэширования статики будет сложнееНо даже с учетом этих нюансов, использование Service Worker кажется более перспективным решением для механизма ретраев.
Альтернативный вариант реализации ретраев - интеграция этого механизма в Webpack.
Мне не нравится тем, что вариант самый не прозрачный и не очевидный, но возможно это дело вкуса)
Как я понимаю, основной вариант реализации ретраев - это расширение возможностей методов webpack_require , например плагин webpack-retry-chunk-load-plugin расширяет webpack_require.e (это require.ensure, который используется для динамических импортов), и в обработчике ошибок загрузки чанка продолжает цепочку промисов повторной загрузкой.
Ссылка на плагин - https://github.com/mattlewis92/webpack-retry-chunk-load-plugin
Никогда не помешает ссылка на великолепный инструмент workbox - https://developers.google.com/web/tools/workbox
Давно не заходил к ним на сайт, появился раздел с рецептами, в том числе пример offline fallback - несколько лет назад мне очень не хватало этого рецепта) https://developers.google.com/web/tools/workbox/modules/workbox-recipes?hl=en#offline_fallback
GitHub
GitHub - mattlewis92/webpack-retry-chunk-load-plugin: A webpack plugin to retry loading of chunks that failed to load
A webpack plugin to retry loading of chunks that failed to load - mattlewis92/webpack-retry-chunk-load-plugin
👍1
Привет!
Несколько дней назад на Github состоялся релиз фреймворка
На самом деле, это нельзя назвать новым релизом - во-первых,
В open source уже существует несколько интересных SSR фреймворков (если говорить про React экосистему): Next.js, Fusion.js от Uber, Umi.js, Remix от создателей react-router.
Отдельно хочу отметить Next.js, как самый активно развивающийся и передовой инструмент.
Мейнтейнеры Next, к примеру, в данный момент портируют используемые
Я бы хотел в серии постов рассказать об особенностях tramvai , которые считаю уникальными, и которыми продолжаю вдохновляться, и возможно немного о наших планах на будущее.
И конечно ссылочки:
- Next.js - https://github.com/vercel/next.js/
- Fusion.js - https://github.com/fusionjs/fusionjs
- Umi.js - https://github.com/umijs/umi
- Remix - https://remix.run/features
Несколько дней назад на Github состоялся релиз фреймворка
tramvai - https://github.com/TinkoffCreditSystems/tramvaitramvai - это фреймворк для создания SSR приложений на React, внутренняя разработка Тинькофф, и последние полтора года я работаю в небольшой технической команде, которая занимается поддержкой этого инструмента.На самом деле, это нельзя назвать новым релизом - во-первых,
tramvai развивается уже много лет, во-вторых, в рамках постепенной миграции фреймворка в open source, мы настроили зеркалирование всех изменений кодовой базы на Github, но продолжаем вести разработку внутри компании.В open source уже существует несколько интересных SSR фреймворков (если говорить про React экосистему): Next.js, Fusion.js от Uber, Umi.js, Remix от создателей react-router.
Отдельно хочу отметить Next.js, как самый активно развивающийся и передовой инструмент.
Мейнтейнеры Next, к примеру, в данный момент портируют используемые
babel плагины на Rust, для интеграции swc и радикального улучшения Developer Experience!Я бы хотел в серии постов рассказать об особенностях tramvai , которые считаю уникальными, и которыми продолжаю вдохновляться, и возможно немного о наших планах на будущее.
И конечно ссылочки:
- Next.js - https://github.com/vercel/next.js/
- Fusion.js - https://github.com/fusionjs/fusionjs
- Umi.js - https://github.com/umijs/umi
- Remix - https://remix.run/features
Одна из ключевых особенностей
Встроенный механизм внедрения зависимостей позволяет собирать приложений из модулей как конструктор из кубиков, и при хорошем проектировании модулей, открывает потрясающие возможности для расширения или замены возможностей в нашем приложении.
Dependency Injection все активнее используется в мире фронтенда, но как мне кажется, ассоциируется только с Angular или Nest.js.
При этом увидеть использование DI контейнеров в React приложениях можно гораздо реже.
Учитывая активное развитие SSR приложений, меня даже немного удивляет, что этот паттерн не получил широкое распространение, далее постараюсь объяснить почему.
Несколько лет назад я занимался разработкой базовой инфраструктуры для миграции PHP приложения (с классической связкой - шаблонизатор на сервере и React на клиенте) на универсальное React приложения (SSR на NodeJS), в онлайн кинотеатре ivi.ru.
Начиная запускать код существующего клиентского JS приложения на сервере, сталкиваешься с рядом проблем, основная из которых - "универсальность" кода, т.е. насколько код сильно завязан на окружение, в котором будет выполняться.
Т.е. все обращения к
Как раз в это время я читал замечательную книгу "Шаблоны проектирования Node.JS", в которой познакомился с Dependency Injection и его реализациями в виде DI container или Service Locator. К слову, раньше мне очень трудно давалось понимание этого паттерна)
В какой-то момент в голове сложились все паззлы, и пришло понимание что DI дает нам очень удобную абстракцию для универсального кода, и лучше всего рассмотреть это на примере работы с
Для начала, пример итогового использования сервиса:
Далее, напишем реализации интерфейса CookieService:
И остается только зарегистрировать нужные реализации в подходящем окружении:
Таким образом мы можем практически полностью избавиться от постоянных проверок в коде на текущее окружение.
Также, мы можем изолировать DI на разные запросы в приложение, и использовать уникальные Request и Response, создавая отдельный DI контейнер на каждый запрос.
Другая фича, которую дает DI, и которая реализована в Angular, Nest.js и tramvai - это возможность расширения приложения отдельными модулями, дополняющими приложение конкретным функционалом с помощью набора токенов и провайдеров (интерфейсы и реализации зависимостей).
Если посмотреть на другие фронтенд фреймворки, можно увидеть разные механизмы для расширения кода - системы плагинов, огромные файлы конфигураци, или паттерн middleware.
tramvai- Dependency Injection.
Встроенный механизм внедрения зависимостей позволяет собирать приложений из модулей как конструктор из кубиков, и при хорошем проектировании модулей, открывает потрясающие возможности для расширения или замены возможностей в нашем приложении.
Dependency Injection все активнее используется в мире фронтенда, но как мне кажется, ассоциируется только с Angular или Nest.js.
При этом увидеть использование DI контейнеров в React приложениях можно гораздо реже.
Учитывая активное развитие SSR приложений, меня даже немного удивляет, что этот паттерн не получил широкое распространение, далее постараюсь объяснить почему.
Несколько лет назад я занимался разработкой базовой инфраструктуры для миграции PHP приложения (с классической связкой - шаблонизатор на сервере и React на клиенте) на универсальное React приложения (SSR на NodeJS), в онлайн кинотеатре ivi.ru.
Начиная запускать код существующего клиентского JS приложения на сервере, сталкиваешься с рядом проблем, основная из которых - "универсальность" кода, т.е. насколько код сильно завязан на окружение, в котором будет выполняться.
Т.е. все обращения к
location, работа с
localStorage, чтение
cookies, не должны происходить на сервере, либо должны содержать проверки на окружение, и например читать куки не из
document.cookies, а из
req.cookies, если мы используем
expressна сервере.
Как раз в это время я читал замечательную книгу "Шаблоны проектирования Node.JS", в которой познакомился с Dependency Injection и его реализациями в виде DI container или Service Locator. К слову, раньше мне очень трудно давалось понимание этого паттерна)
В какой-то момент в голове сложились все паззлы, и пришло понимание что DI дает нам очень удобную абстракцию для универсального кода, и лучше всего рассмотреть это на примере работы с
cookies.
Для начала, пример итогового использования сервиса:
// создадим общий контейнер зависимостей
const di = createContainer();
// опишем интерфейс сервиса для работы с куками
interface CookieService {
get(key): string;
set(key, value, options): void;
}
// React компонент, который завязан только на интерфейс, но не на реализацию
const Component = ({ cookieKey }) => {
// не важно, на сервере или на клиенте будет отрендерен компонент,
// главное что бы в di была подходящая реализация сервиса
const cookieService = useDI<CookieService>('cookie service');
const cookieValue = cookieService.get(cookieKey);
return ...;
}
Далее, напишем реализации интерфейса CookieService:
// серверная реализация, работает с объектом Request
class ServerCookieService implements CookieService {
constructor(request: Request) {}
get(key) { ... }
set(key, value, options) { ... }
}
// серверная реализация, работает с document.cookie
class ClientCookieService implements CookieService {
constructor() {}
get(key) { ... }
set(key, value, options) { ... }
}
И остается только зарегистрировать нужные реализации в подходящем окружении:
// server.js
// получаем зависимость
const request = di.get('request');
// регистрируем зависимость на сервере
di.provide('cookie service', new ServerCookieService(request));
// client.js
// регистрируем зависимость на клиенте
di.provide('cookie service', new ClientCookieService());
Таким образом мы можем практически полностью избавиться от постоянных проверок в коде на текущее окружение.
Также, мы можем изолировать DI на разные запросы в приложение, и использовать уникальные Request и Response, создавая отдельный DI контейнер на каждый запрос.
Другая фича, которую дает DI, и которая реализована в Angular, Nest.js и tramvai - это возможность расширения приложения отдельными модулями, дополняющими приложение конкретным функционалом с помощью набора токенов и провайдеров (интерфейсы и реализации зависимостей).
Если посмотреть на другие фронтенд фреймворки, можно увидеть разные механизмы для расширения кода - системы плагинов, огромные файлы конфигураци, или паттерн middleware.
На мой взгляд, модульный подход дает максимальную гибкость, и позволяет сделать минимальное зацепление кода у различных модулей.
Подробнее про реализацию DI в tramvai можно почитать в документации - https://tramvai.dev/docs/concepts/overview
И ссылочки на различные DI контейнеры:
- https://github.com/inversify/InversifyJS
- https://github.com/mgechev/injection-js
- https://github.com/microsoft/tsyringe
- https://github.com/typestack/typedi
Многие из этих библиотек используют механизм рефлексии, позволяющий делать магию с DI - https://www.npmjs.com/package/reflect-metadata
Рефлексия имеет ряд плюсов и недостатков, и на данный момент не используется в tramvai.
Подробнее про реализацию DI в tramvai можно почитать в документации - https://tramvai.dev/docs/concepts/overview
И ссылочки на различные DI контейнеры:
- https://github.com/inversify/InversifyJS
- https://github.com/mgechev/injection-js
- https://github.com/microsoft/tsyringe
- https://github.com/typestack/typedi
Многие из этих библиотек используют механизм рефлексии, позволяющий делать магию с DI - https://www.npmjs.com/package/reflect-metadata
Рефлексия имеет ряд плюсов и недостатков, и на данный момент не используется в tramvai.
tramvai.dev
Introduction to tramvai | tramvai
tramvai is a lightweight web framework for building SSR applications with a modular system and DI to quickly extend the functionality of applications.
Привет!
Хочу рассказать еще про одну особенность tramvai, связанную с жизненным циклом приложения, и запросами в это приложение.
Для начала рассмотрим несколько популярных вариантов, как запускаются и конфигурируются стандартные приложения, написанные на Express или React.
Если это
Если это
И это подходит для многих приложений, но при этом несет ряд проблем, при росте кодовой базы:
- Добавление новой независимой фичи - со своими страницами, сторами, провайдерами - потребует вносить изменения в несколько разных мест
- Не связанный код зацеплен друг с другом
- Если мы говорим про middleware, не связанный код может еще и ломать друг друга, подробнее в классном докладе - https://www.youtube.com/watch?v=RS8x73z4csI
- Сайд-эффекты могут быть размазаны по всему приложению, и для серверного рендеринга, нужно постараться, что бы выполнить все асинхронные действия до
А если в нашем SSR приложении мы хотим рендерить страницу как можно быстрее?
Для этого нужно запускать асинхронные действия максимально параллельно, и иметь возможность исключить действия, выходящие за определенный таймаут.
Затем, было бы неплохо выполнить эти сайд-эффекты, которые не уложились в тайминги, повторно на клиенте.
Звучит как не тривиальная задача, и некоторые из фреймворков пытаются решить ее тем или иным способом, например
Если обобщить, помимо производительности, страдает расширяемость и модульность приложения.
Хочу рассказать еще про одну особенность tramvai, связанную с жизненным циклом приложения, и запросами в это приложение.
Для начала рассмотрим несколько популярных вариантов, как запускаются и конфигурируются стандартные приложения, написанные на Express или React.
Если это
express сервер, обычно создается одна точка входа, где создается приложение, к нему добавляются роуты и middleware, и запускается сервер через app.listenЕсли это
React приложение, мы так же в большинстве случаев увидим одну точку входа, где происходит инициализация стейт-менеджера, композиция провайдеров, и создание App компонента. Если это SSR, то в серверной реализации App дополнительно будет логика по загрузке всех данных для приложения, а в клиентской реализации - гидрация этих данных.И это подходит для многих приложений, но при этом несет ряд проблем, при росте кодовой базы:
- Добавление новой независимой фичи - со своими страницами, сторами, провайдерами - потребует вносить изменения в несколько разных мест
- Не связанный код зацеплен друг с другом
- Если мы говорим про middleware, не связанный код может еще и ломать друг друга, подробнее в классном докладе - https://www.youtube.com/watch?v=RS8x73z4csI
- Сайд-эффекты могут быть размазаны по всему приложению, и для серверного рендеринга, нужно постараться, что бы выполнить все асинхронные действия до
renderToString нашего приложенияА если в нашем SSR приложении мы хотим рендерить страницу как можно быстрее?
Для этого нужно запускать асинхронные действия максимально параллельно, и иметь возможность исключить действия, выходящие за определенный таймаут.
Затем, было бы неплохо выполнить эти сайд-эффекты, которые не уложились в тайминги, повторно на клиенте.
Звучит как не тривиальная задача, и некоторые из фреймворков пытаются решить ее тем или иным способом, например
Appolo просто рендерит наше приложение на сервере несколько раз, пока не выполнит все query - и это очень не эффективно, т.к. renderToString это самая медленная часть нашего SSR, к тому же синхронно блокирующий event loop.Если обобщить, помимо производительности, страдает расширяемость и модульность приложения.
YouTube
Node.js Middleware – никогда больше! [ru] / Тимур Шемсединов
Видео с онлайн-конференции JavaScript fwdays'20 autumn, которая прошла 22 сентября 2020 года.
Описание доклада:
Почему приложение работает нестабильно, происходит утечка памяти и процесс часто вылетает? Почему вам сложно найти ошибку и нужно долго делать…
Описание доклада:
Почему приложение работает нестабильно, происходит утечка памяти и процесс часто вылетает? Почему вам сложно найти ошибку и нужно долго делать…
Что интересного предлагает
С помощью Dependency Injection и механизма Command Line Chain, фреймворк дает возможность вклиниться на ряд этапов жизненного цикла приложения:
- Различные этапы инициализации веб-сервера (т.к. мы используем
- Различные этапы запуска
- Этапы для загрузки данных, необходимых для рендеринга страницы (один из них как раз создан для сайд-эффектов)
- Этап генерации страницы (на нем сервер делает
- Этапы для загрузки данных при SPA-переходах
Механизм Command Line Chain во-первых, предоставляет наглядный флоу жизни приложения и запросов.
Во-вторых, выполняет все действия параллельно, на каждом этапе.
Во-вторых, вместе с механизмом Actions, позволяет установить таймауты на выполнение действий, отдать ответ на клиент не дожидаясь этих действий, и выполнить их повторно на клиенте.
Пример добавления нового действия на этап получения информации о пользователе, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/concepts/command-line-runner#resolve_user_deps
При необходимости, с помощью провайдеров можно предоставлять хуки и guard'ы для переходов роутера, например для проверки доступов пользователя к конкретной странице - https://tramvai.dev/docs/references/libs/router#router-guards
Также, есть провайдеры, позволяющие добавить скрипты, стили или meta-теги в HTML разметку итоговой страницы.
Пример такого провайдера, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/references/modules/render#как-добавить-загрузку-ассетов-на-странице
Такая гибкость дает нам возможность создавать отдельные модули, которые по сути являются списком провайдеров, вместе реализующих определенную фичу, и подключать эти модули простым добавлением в список modules при создании нашего приложения.
tramvai, для решения этих кейсов?С помощью Dependency Injection и механизма Command Line Chain, фреймворк дает возможность вклиниться на ряд этапов жизненного цикла приложения:
- Различные этапы инициализации веб-сервера (т.к. мы используем
express под капотом, можно добавить express middleware или роуты)- Различные этапы запуска
listen сервера (можно добавить обработчики uncaughtException и unhandledRejection , запустить на другом порту мокер, или static сервер)- Этапы для загрузки данных, необходимых для рендеринга страницы (один из них как раз создан для сайд-эффектов)
- Этап генерации страницы (на нем сервер делает
renderToString, а клиент hydrate)- Этапы для загрузки данных при SPA-переходах
Механизм Command Line Chain во-первых, предоставляет наглядный флоу жизни приложения и запросов.
Во-вторых, выполняет все действия параллельно, на каждом этапе.
Во-вторых, вместе с механизмом Actions, позволяет установить таймауты на выполнение действий, отдать ответ на клиент не дожидаясь этих действий, и выполнить их повторно на клиенте.
Пример добавления нового действия на этап получения информации о пользователе, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/concepts/command-line-runner#resolve_user_deps
// загружаем в store данные, необходимые для последующего рендеринга страницы
{
provide: commandLineListTokens.resolveUserDeps,
multi: true,
useFactory: ({ store }) => {
return async function updateCounterStore() {
await someAsyncAction();
store.dispatch(incrementEvent());
};
},
deps: {
store: STORE_TOKEN,
},
}
При необходимости, с помощью провайдеров можно предоставлять хуки и guard'ы для переходов роутера, например для проверки доступов пользователя к конкретной странице - https://tramvai.dev/docs/references/libs/router#router-guards
Также, есть провайдеры, позволяющие добавить скрипты, стили или meta-теги в HTML разметку итоговой страницы.
Пример такого провайдера, и документация - https://coretech-frontend.pages.devplatform.tcsbank.ru/tramvai/docs/references/modules/render#как-добавить-загрузку-ассетов-на-странице
// регистрируем meta viewport, который будет добавлятся на каждую страницу
{
provide: RENDER_SLOTS,
multi: true,
useValue: {
type: ResourceType.asIs,
slot: ResourceSlot.HEAD_META,
payload:
'<meta name="viewport" content="width=device-width, initial-scale=1">',
},
}
Такая гибкость дает нам возможность создавать отдельные модули, которые по сути являются списком провайдеров, вместе реализующих определенную фичу, и подключать эти модули простым добавлением в список modules при создании нашего приложения.
tramvai.dev
router | tramvai
Routing library. It can work both on the server and on the client. Designed primarily for building isomorphic applications.
Привет!
В ближайших постах хочу рассказать про устройство монорепозитория
Наш приватный репозиторий содержит около 150 npm библиотек, инфраструктуру для сборки, тестирования и публикации этих пакетов, сайт документации и различные утилиты.
Версиями пакетов, публикациями и генерацией ченджлогов управляет внутренняя разработка Тинькофф - библиотека
На каждый мерж в master ветку,
На каждый релизный тег запускается пайплайн публикации пакетов в приватный npm registry, и одновременно зеркалирование публичного кода на Github.
Для управления версиями пакетов в репозитории мы используем два важных подхода - сквозное версионирование, и хранение версий в релизных тегах.
Сквозное версионирование используется для всех пакетов, имеющий непосредственное отношение к фреймворку (cli, ядро фреймворка, многочисленные модули), этот термин означает что все unified пакеты общую версию, и должны обновляться одновременно.
Такой подход вы можете увидеть у
Основной плюс unified версионирования - гарантируется совместимость между пакетами одной версии. Это очень крутая возможность, т.к. раньше у пользователя был только один простой вариант поднять версию фреймворка, не потеряв совместимость - устанавливать все пакеты latest версии.
Один из минусов подхода - любое обновление пакета из списка unified, требует поднять версии и опубликовать все эти пакеты из списка, что значительно замедляет CI.
Для удобной работы со сквозными версиями tramvai предлагает два механизма:
- cli команда
- утилита
Хранение версий в релизных тегах само по себе не дает преимуществ, и у нас используется вместе со stub версиями пакетов в исходных
Допустим, у нас был пакет с зависимостями:
Раньше, каждый крупный Merge Request сопровождался конфликтами, если в master ветке обновлялась версии пакетов , а затронутые библиотеки в MR содержали изменения в
Теперь, наш пакет выглядит так:
Версия
Раньше я не встречался с таким подходом, и в целом не работал с монорепозиториями, но на своем опыте увидел, как это упростило разработку.
Также, для монорепы мы используем
На Github репозитории, для публикации кода в npm, версии пакетов поставляются в файле https://github.com/TinkoffCreditSystems/tramvai/blob/main/packages-versions.json
В ближайших постах хочу рассказать про устройство монорепозитория
tramvai, и про решение зеркалировать публичный код на Github, вместо полной миграции.Наш приватный репозиторий содержит около 150 npm библиотек, инфраструктуру для сборки, тестирования и публикации этих пакетов, сайт документации и различные утилиты.
Версиями пакетов, публикациями и генерацией ченджлогов управляет внутренняя разработка Тинькофф - библиотека
pvm. На каждый мерж в master ветку,
pvm определяет какие пакеты были изменены, поднимает их версии согласно conventional commits, и создает релизный тег, где и хранятся все версии. На каждый релизный тег запускается пайплайн публикации пакетов в приватный npm registry, и одновременно зеркалирование публичного кода на Github.
Для управления версиями пакетов в репозитории мы используем два важных подхода - сквозное версионирование, и хранение версий в релизных тегах.
Сквозное версионирование используется для всех пакетов, имеющий непосредственное отношение к фреймворку (cli, ядро фреймворка, многочисленные модули), этот термин означает что все unified пакеты общую версию, и должны обновляться одновременно.
Такой подход вы можете увидеть у
Angular , и с некоторыми ограничениями, в монорепозиториях использующих Lerna - https://github.com/lerna/lerna#fixedlocked-mode-defaultОсновной плюс unified версионирования - гарантируется совместимость между пакетами одной версии. Это очень крутая возможность, т.к. раньше у пользователя был только один простой вариант поднять версию фреймворка, не потеряв совместимость - устанавливать все пакеты latest версии.
Один из минусов подхода - любое обновление пакета из списка unified, требует поднять версии и опубликовать все эти пакеты из списка, что значительно замедляет CI.
Для удобной работы со сквозными версиями tramvai предлагает два механизма:
- cli команда
tramvai update для легкого обновления версий - утилита
@tramvai/tools-check-versions, которая поставляется вместе с core пакетом, и на этапе postinstall проверяет версии установленных зависимостей в приложении. Хранение версий в релизных тегах само по себе не дает преимуществ, и у нас используется вместе со stub версиями пакетов в исходных
package.json файлах.Допустим, у нас был пакет с зависимостями:
{
"name": "@tramvai/foo",
"version": "0.1.0",
"dependencies": {
"@tramvai/bar": "^1.1.0",
"@tramvai/baz": "^2.0.0"
}
}
Раньше, каждый крупный Merge Request сопровождался конфликтами, если в master ветке обновлялась версии пакетов , а затронутые библиотеки в MR содержали изменения в
dependencies. Теперь, наш пакет выглядит так:
{
"name": "@tramvai/foo",
"version": "0.0.0-stub",
"dependencies": {
"@tramvai/bar": "0.0.0-stub",
"@tramvai/baz": "0.0.0-stub"
}
}
Версия
0.0.0-stub никогда не вызовет конфликтов слияния, а вычисление реальных версий происходит только в CI - при создании нового релизного тега и публикации, внутри библиотеки pvm. Раньше я не встречался с таким подходом, и в целом не работал с монорепозиториями, но на своем опыте увидел, как это упростило разработку.
Также, для монорепы мы используем
yarn workspaces , и воркспейсы замечательно работают со stub версиями. На Github репозитории, для публикации кода в npm, версии пакетов поставляются в файле https://github.com/TinkoffCreditSystems/tramvai/blob/main/packages-versions.json
Про development и production сборку пакетов.
Все tramvai библиотеки написаны на
Для локальной разработки, мы используем
Project References дают возможность одной командой запускать сборку всех пакетов в репозитории, и при этом делать пересборку только измененного кода.
Также, при пересборке учитываются, кто зависит от изменившегося пакета, и выполняется пересборка итогового графа зависимостей - это позволяет сразу увидеть, как изменения одного пакета могут поломать другие.
Минимальное время старта команды
К сожалению, Typenoscript не дает инструмента для автоматической генерации Project References и поддержки их в актуальном состоянии - поэтому в tramvai монорепе используется самописная утилита
Вторая проблема Project References - это отсутствие сгенерированных
Мы решаем эту проблему так:
- В
- Перед публикацией, все поля
Для эффективной production сборки библиотек мы используем
- es2019 код с CommonJS модулями для запуска через старые версии NodeJS, бандл будет указан в
- es2019 код с ES modules для современных бандлеров (
- И если для пакета указано поле brow
Таким образом, на пакет, который указывает отдельные точки входа для серверной и клиентской сборки, сгенерируется три бандла - serv
Почему мы публикуем код в es20
Фронтенд библиотеки в Тинькофф (в части проектов, в основном это React экосистема) следуют принципу, когда публикуются только современный JavaScript код, а разработчики приложений транспилируют зависимости в подходящий стандарт кода.
Немного мотивации к публикации modern кода:
- https://web.dev/publish-modern-javanoscript/
- https://github.com/sindresorhus/ama/issues/446
- https://dev.to/garylchew/bringing-modern-javanoscript-to-libraries-432c
- https://babeljs.io/blog/2018/06/26/on-consuming-and-publishing-es2015+-packages
@tra
- Общий serv
- Для клиентского кода у нас есть два набора brow
Кажется это должно улучшаться в лучшую сторону с каждым обновлением существующих JS движков.
В итоге, мы получили неплохой Developer Experience при разработке в монорепе, и поставляем код библиотек минимального размера, с поддержкой tree-shaking и различных target окружений, для наших пользователей.
Все tramvai библиотеки написаны на
Typenoscript, и комфортная разработка требует тщательной настройки инфраструктуры монорепозитория. Для локальной разработки, мы используем
tsc и Project References - https://www.typenoscriptlang.org/docs/handbook/project-references.html, в этой связке watch режим имеет минимальное время старта, и очень быструю пересборку. Project References дают возможность одной командой запускать сборку всех пакетов в репозитории, и при этом делать пересборку только измененного кода.
Также, при пересборке учитываются, кто зависит от изменившегося пакета, и выполняется пересборка итогового графа зависимостей - это позволяет сразу увидеть, как изменения одного пакета могут поломать другие.
Минимальное время старта команды
tsc --build --watch достигается за счет кэширования информации о сборке каждого пакета.К сожалению, Typenoscript не дает инструмента для автоматической генерации Project References и поддержки их в актуальном состоянии - поэтому в tramvai монорепе используется самописная утилита
@tramvai-monorepo/fix-ts-references Вторая проблема Project References - это отсутствие сгенерированных
.d.ts файлов при первой сборке, и возможные ошибки этой сборки. Мы решаем эту проблему так:
- В
package.json каждого пакета поле typings смотрит на исходные .ts файлы в папке src - Перед публикацией, все поля
typings заменяются на собранные .d.ts файлы из папки libДля эффективной production сборки библиотек мы используем
rollup, поверх которого написана утилита @tramvai/build - https://tramvai.dev/docs/references/tools/build @tramvai/build собирает код каждого пакета в несколько общих бандлов: - es2019 код с CommonJS модулями для запуска через старые версии NodeJS, бандл будет указан в
"main" поле в package.json - es2019 код с ES modules для современных бандлеров (
@tramvai/cli использует webpack@5), бандл будет указан в "module" поле в package.json - И если для пакета указано поле brow
ser в package.json, собирается отдельный es2019 код с ES modules, содержащий код для браузерного окружения Таким образом, на пакет, который указывает отдельные точки входа для серверной и клиентской сборки, сгенерируется три бандла - serv
er.js, server.es.js и browser.js
Спецификацию поля browser можно посмотреть тут - https://github.com/defunctzombie/package-browser-field-spec Почему мы публикуем код в es20
19, вместо es5? Фронтенд библиотеки в Тинькофф (в части проектов, в основном это React экосистема) следуют принципу, когда публикуются только современный JavaScript код, а разработчики приложений транспилируют зависимости в подходящий стандарт кода.
Немного мотивации к публикации modern кода:
- https://web.dev/publish-modern-javanoscript/
- https://github.com/sindresorhus/ama/issues/446
- https://dev.to/garylchew/bringing-modern-javanoscript-to-libraries-432c
- https://babeljs.io/blog/2018/06/26/on-consuming-and-publishing-es2015+-packages
@tra
mvai/cli собирает бандлы приложения по такой логике: - Общий serv
er.js бандл с серверным кодом, содержащий все зависимости, и код стандарта es5.
Интересный факт, эксперименты с использованием es2019/es2020 кода на сервере показали ухудшение производительности примерно на 10%! - Для клиентского кода у нас есть два набора brow
serslist конфигов - default и modern, и es2019 код библиотек транспилируется в формат для целевых браузеров с помощью babel, генерируются отдельные чанки с default и modern кодом, и затем для браузера выбираются нужные через проверку User-Agent
Второй интересный факт, modern сборка может весить значительно меньше default, но как и в случае с серверным кодом, может работать медленнее. Кажется это должно улучшаться в лучшую сторону с каждым обновлением существующих JS движков.
В итоге, мы получили неплохой Developer Experience при разработке в монорепе, и поставляем код библиотек минимального размера, с поддержкой tree-shaking и различных target окружений, для наших пользователей.
www.typenoscriptlang.org
Documentation - Project References
How to split up a large TypeScript project
Добавлю парочку полезных ссылок:
Топовые советы в Wiki Typenoscript по ускорению сборки, об этой страничке я узнал в канале @wild_wild_web - https://github.com/microsoft/TypeScript/wiki/Performance
Например, совет по ограничению поля "types" почти в два раза ускорил нашу production сборку - https://github.com/microsoft/TypeScript/wiki/Performance#controlling-types-inclusion
Проблема была в том, что монорепозиторий содержит огромное количество тайпингов в зависимостях, и для транспиляции даже файла с одной строчкой кода, TS тратил очень много времени на резолв этих тайпингов, из которых реально нужные, без прямого импорта - это
Утилита для сборки пакетов
И мощный фреймворк для организации монорепозиториев NX - https://nx.dev/
Это достаточно сложный для освоения и интеграции инструмент, но потенциально может в разы ускорять CI процессы в больших монорепозиториях.
Пример работы инструмента Nx Cloud, с распределенным выполнением задач и кэшированием - https://www.youtube.com/watch?v=Exs64pscwxA&ab_channel=Nrwl-NarwhalTechnologiesInc.
Топовые советы в Wiki Typenoscript по ускорению сборки, об этой страничке я узнал в канале @wild_wild_web - https://github.com/microsoft/TypeScript/wiki/Performance
Например, совет по ограничению поля "types" почти в два раза ускорил нашу production сборку - https://github.com/microsoft/TypeScript/wiki/Performance#controlling-types-inclusion
Проблема была в том, что монорепозиторий содержит огромное количество тайпингов в зависимостях, и для транспиляции даже файла с одной строчкой кода, TS тратил очень много времени на резолв этих тайпингов, из которых реально нужные, без прямого импорта - это
node и jestУтилита для сборки пакетов
microbundle, наш референс при разработке @tramvai/build - https://github.com/developit/microbundleИ мощный фреймворк для организации монорепозиториев NX - https://nx.dev/
Это достаточно сложный для освоения и интеграции инструмент, но потенциально может в разы ускорять CI процессы в больших монорепозиториях.
Пример работы инструмента Nx Cloud, с распределенным выполнением задач и кэшированием - https://www.youtube.com/watch?v=Exs64pscwxA&ab_channel=Nrwl-NarwhalTechnologiesInc.
GitHub
Performance
TypeScript is a superset of JavaScript that compiles to clean JavaScript output. - microsoft/TypeScript
👍1
Привет!
Про выход tramvai в open source.
Зачем это нужна для команды разработчиков, и для Тинкофф в целом?
Одна из важнейших вещей, это еще одно громкое заявление - "В Тинькофф крутой фронтенд!"
Разработчики Тинькофф делают все больше отличных open source проектов, пишут все больше интересных статей, выступают на конференциях - из таких активностей формируется образ компании, привлекательный для новых разработчиков.
Мы активно нанимаем коллег, и у нас классные интервьюеры 😉
Для мейнтейнеров tramvai, это новый источник вдохновения для развития фреймворка, возможность получить свежий взгляд со стороны.
И один из плюсов, это быстрое прототипирование с помощью JS песочниц, недавно мы добавили базовый шаблон трамвая на Codesandbox - https://codesandbox.io/s/tramvai-new-qgk90
Далее, опишу ряд проблем, которые могут стоять на пути у проприетарной разработки, усложняющие переход в OSS:
- Код, документация и история VCS могут содержать чувствительную информацию - самый простой пример, это ссылки на внутренние ресурсы
- Часть пакетов подходит для OSS, часть предназначена только для внутреннего использования
- Пакеты публикуются в приватный регистр пакетов
- Проект использует приватный CI
- Отсутствут локализация документации
Таким образом, полный переход Github требует отделить публичную часть проекта, перенести код без сохранения истории коммитов, настроить в Github Actions линтинг, unit и интеграционное тестирование, версионирование и публикацию пакетов, перевод, сборку и деплой документации.
Например, такой переход осуществил огненный Angular ui-kit от наших коллег - https://taiga-ui.dev/
Taiga содержит ряд проприетарных компонентов в приватном репозитории внутри Тинькофф, и приватный репозитрий использует публичный с помощью git submodules.
Почему этот путь не подошел для tramvai:
- Теряется история коммитов - это важный источник информации для разработчика
- Проект имеет достаточно сложный CI, и в версионировании и публикации пакетов участвует другая внутренняя разработка Тинькофф, о которой я писал в одном из предыдущих постов
- Все tramvai пакеты объединены сквозным версионированием, отделение публичных означает, что два набора пакетов разойдутся в версиях, и вернутся проблемы, связанные с обновлением зависимостей
- Не удобно делать глобальный рефакторинг, который затрагивает все пакеты
Каждая из этих проблем кажется неизбежным злом, но вместе порождают очень много сложностей, которые мы постарались решить инкрементальным выходом в OSS.
Про выход tramvai в open source.
Зачем это нужна для команды разработчиков, и для Тинкофф в целом?
Одна из важнейших вещей, это еще одно громкое заявление - "В Тинькофф крутой фронтенд!"
Разработчики Тинькофф делают все больше отличных open source проектов, пишут все больше интересных статей, выступают на конференциях - из таких активностей формируется образ компании, привлекательный для новых разработчиков.
Мы активно нанимаем коллег, и у нас классные интервьюеры 😉
Для мейнтейнеров tramvai, это новый источник вдохновения для развития фреймворка, возможность получить свежий взгляд со стороны.
И один из плюсов, это быстрое прототипирование с помощью JS песочниц, недавно мы добавили базовый шаблон трамвая на Codesandbox - https://codesandbox.io/s/tramvai-new-qgk90
Далее, опишу ряд проблем, которые могут стоять на пути у проприетарной разработки, усложняющие переход в OSS:
- Код, документация и история VCS могут содержать чувствительную информацию - самый простой пример, это ссылки на внутренние ресурсы
- Часть пакетов подходит для OSS, часть предназначена только для внутреннего использования
- Пакеты публикуются в приватный регистр пакетов
- Проект использует приватный CI
- Отсутствут локализация документации
Таким образом, полный переход Github требует отделить публичную часть проекта, перенести код без сохранения истории коммитов, настроить в Github Actions линтинг, unit и интеграционное тестирование, версионирование и публикацию пакетов, перевод, сборку и деплой документации.
Например, такой переход осуществил огненный Angular ui-kit от наших коллег - https://taiga-ui.dev/
Taiga содержит ряд проприетарных компонентов в приватном репозитории внутри Тинькофф, и приватный репозитрий использует публичный с помощью git submodules.
Почему этот путь не подошел для tramvai:
- Теряется история коммитов - это важный источник информации для разработчика
- Проект имеет достаточно сложный CI, и в версионировании и публикации пакетов участвует другая внутренняя разработка Тинькофф, о которой я писал в одном из предыдущих постов
- Все tramvai пакеты объединены сквозным версионированием, отделение публичных означает, что два набора пакетов разойдутся в версиях, и вернутся проблемы, связанные с обновлением зависимостей
- Не удобно делать глобальный рефакторинг, который затрагивает все пакеты
Каждая из этих проблем кажется неизбежным злом, но вместе порождают очень много сложностей, которые мы постарались решить инкрементальным выходом в OSS.
Первый этап, на котором сейчас находится проект - это зеркалирование публичного кода на Github, при этом вся разработка ведется в приватном репозитории.
На этапе исследования, мы нашли мощный инструмент, созданный для аналогичных целей внутри Google,
При чем, кажется это вторая или третья итерация по созданию такого инструмента от разработчиков Google.
Кстати, для copybara есть гибкая обвязка под Github Actions - https://github.com/Olivr/copybara-action и публичный Docker образ - https://github.com/anipos/copybara-docker-image
Каждый релиз трамвая, запускается джоба с генерацией конфига и запуском copybara.
Конфиг содержит список публичных файлов и трансформации для кода - например, замена ссылок приватной документации на https://tramvai.dev
Copybara проверяет изменения с последнего коммита в Github репе, схлопывает все новые коммиты в один релизный коммит, и пушить его на Github.
Затем в Github Actions запускается сборка и деплой документации, и сборки и публикация пакетов в npm.
Я несколько раз накосячил с публикацией пакетов и откатывал их, оказалось, что npm запрещает публикацию удаленных пакетов в течение 24 часов, и запрещает публиковать ту же версию, что была удалена 😥
По итогу, мы всегда имеем актуальный код на Github, наш фреймворк доступен для публичного использования, и при этом наши процессы разработки не поменялись.
Следующий шаг миграции в OSS - это зеркалирование MR от контрибьюторов из Github в наш приватный репозиторий, на данный момент мы можем делать это только вручную.
Двусторонняя синхронизация репозиториев это потенциальный источник проблем, поэтому мы не спешим с переходом на этот этап.
В будущем хочется полностью перейти на Github, отказавшись от синхронизации репозиториев.
На этапе исследования, мы нашли мощный инструмент, созданный для аналогичных целей внутри Google,
copybara - https://github.com/google/copybara.При чем, кажется это вторая или третья итерация по созданию такого инструмента от разработчиков Google.
Кстати, для copybara есть гибкая обвязка под Github Actions - https://github.com/Olivr/copybara-action и публичный Docker образ - https://github.com/anipos/copybara-docker-image
Каждый релиз трамвая, запускается джоба с генерацией конфига и запуском copybara.
Конфиг содержит список публичных файлов и трансформации для кода - например, замена ссылок приватной документации на https://tramvai.dev
Copybara проверяет изменения с последнего коммита в Github репе, схлопывает все новые коммиты в один релизный коммит, и пушить его на Github.
Затем в Github Actions запускается сборка и деплой документации, и сборки и публикация пакетов в npm.
Я несколько раз накосячил с публикацией пакетов и откатывал их, оказалось, что npm запрещает публикацию удаленных пакетов в течение 24 часов, и запрещает публиковать ту же версию, что была удалена 😥
По итогу, мы всегда имеем актуальный код на Github, наш фреймворк доступен для публичного использования, и при этом наши процессы разработки не поменялись.
Следующий шаг миграции в OSS - это зеркалирование MR от контрибьюторов из Github в наш приватный репозиторий, на данный момент мы можем делать это только вручную.
Двусторонняя синхронизация репозиториев это потенциальный источник проблем, поэтому мы не спешим с переходом на этот этап.
В будущем хочется полностью перейти на Github, отказавшись от синхронизации репозиториев.
GitHub
GitHub - google/copybara: Copybara: A tool for transforming and moving code between repositories.
Copybara: A tool for transforming and moving code between repositories. - google/copybara
😁1
