Небольшое безобидное удобство может стать существенно вредным неудобством.
Я работал над веб-сервером NodeJS в Monorepo. У проекта было несколько родственных друг другу проектов npm, каждый из которых запускал свой сервер на другом локальном порту в одном развертываемом файле. Самодельный шлюз / прокси перенаправлял запросы пользователей в каждую отдельную службу.
Один из этих модулей NPM (назовем «PrincipalModule») включал другой родственный модуль npm install --save ../my-dependency
(назовем «DependencyModule»). PrincipalModule добавил зависимость с помощью установки локального модуля NPM:
Вопрос начался с потребности в удобстве.
Я сделал изменение, чтобы использовать app-root-path
в DependencyModule для загрузки компонентов из корня проекта, например const something = require(`${root}/src/path/to/something`);
. Он импортировал некоторые общедоступные классы DependencyModule, которые будут работать в контексте PrincipalModule.
Все работало нормально как при локальной разработке, так и на всем конвейере CI. Все тесты, включая тесты пользовательского интерфейса, которые перед развертыванием запускали бы все серверы вместе, были зелеными. Однако после того, как код попал в производство, я получил SMS-уведомление, в котором говорилось, что prod не работает со следующей ошибкой: «Не удается найти модуль x». x
- это имя одного из классов, импортированных кодом с использованием app-root-path
для разрешения корня.
Это был небольшой проект с небольшим количеством пользователей. Мы откладываем некоторые операционные изменения, которые могут сделать систему доступной на 100%, даже если сервер не запускается. По этой причине ошибка закончилась отключением.
В этом проекте было значительное тестовое покрытие. Если вы посмотрите на код и процесс развертывания, казалось невозможным, чтобы отключение произошло сразу после некоторого изменения кода. Вы ожидаете, что некоторые тесты не пройдут, прежде чем дойдут до этой точки. Конечно, по-прежнему существовала вероятность того, что третья сторона станет мошенником или возникнет какая-то ошибка, и это риск, на который мы могли спокойно пойти с учетом деловых обстоятельств. Однако появление ошибки сразу после развертывания не ожидалось. Это было почти невозможно, так что же происходило?
Я был в недоумении. Интеграционные тесты были зелеными, а все тесты пользовательского интерфейса, в которых использовались реальные серверы, были зелеными. Я попытался запустить те же команды CI для создания приложения, такого как prod. Никаких кубиков. Я не мог воспроизвести проблему.
Происходило что-то интересное.
Система имела приличное покрытие тестами и процесс развертывания выше кривой; Казалось невозможным наблюдать отключение сразу после развертывания. Что происходило?
Я начал некоторую отладку в продукте, развернув некоторое ведение журнала после того, как вернулся к рабочему коммиту. Вот что я нашел:
При запуске DependencyModule в контексте PrincipalModule я ожидал, что app-root-path
разрешит его как корень DependencyModule. Тем не менее, он разрешался как корень модуля PrincipalModule. В режиме локальной разработки и на серверах CI поведение такое, чего я и ожидал: корень был относительно DependencyModule.
После некоторого исследования я выяснил, что когда вы запускаете npm install ../dependency
, NPM автоматически создает символическую ссылку между зависимостью и принципалом; символическая ссылка находится в папке node_modules
Принципала. Модуль app-root-path
использует __dirname
для поиска корня, и из-за символической ссылки он разрешается как корень ../dependency
. Я пробовал удалить все node_modules
, package-lock.json
файлы и переустановить все локально. NPM всегда создает эту символическую ссылку, поэтому я все еще не мог воспроизвести проблему «модуль не найден» локально.
Почему код работает не так, как в продукте? Это та же версия узла, та же версия NPM и та же app-root-path
версия для PrincipalModule и DependencyModule! Я загрузил код из zip-файла прямо из артефакта, сгенерированного конвейером, и попытался запустить его локально.
Вот тогда я наконец воспроизвел ошибку! Вот источник проблемы:
Код не смог найти какой-либо модуль в продукте, потому что символическая ссылка между Принципалом и Зависимостью исчезла. Пакет app-root-path
разрешал корень из PrincipalModule в продукте, но локально он разрешал корень из DependencyModule. Это другой контекст, связанный с этой проблемой, о которой сообщается в проекте app-root-path
.
О, ну ... вы упомянули zip-файл?
Я использовал эластичный бобовый стебель (EBS). EBS требует, чтобы проект был отправлен в виде zip-файла перед запуском команды eb
для загрузки на сервер.
Перед развертыванием процесс сборки заархивирует весь код с помощью командной строки macOS zip
для отправки в EBS. Затем он загружает zip-файл на prod-сервер и распаковывает код для вызова npm start
.
Если вы знаете, как работает символическая ссылка, возможно, вы уже выяснили проблему.
Объединение node_modules
, затем архивирование в процессе сборки и последующее разархивирование в продукте без запуска npm install
удаляет символическую ссылку, которую NPM создал автоматически. Поскольку символическая ссылка исчезла, app-root-path
разрешает корневой путь к PrincipalModule, а не к DependencyModule.
Расположение файлов, импортированных в prod, неверно, и его невозможно воспроизвести при установке проекта на свой компьютер.
Это невероятно, пока вы не найдете первопричину, которая затем станет глупой.
Некоторые уроки для меня
- Архивирование удаляет символическую ссылку. Да, я не очень часто использую символические ссылки, поэтому не знал об этом.
- При использовании NPM осознайте магию символических ссылок, которая происходит за NPM при запуске npm install.
Я начал использовать app-root-path
только для удобства, чтобы импортировать все мои модули из корня. Теперь я научился не слишком полагаться на эти вещи и пару сторонних зависимостей, которые зависят от среды, чтобы делать выводы о системе. В зависимости от этого система склонна к крайним случаям, которые было бы довольно сложно отладить.
Некоторые рекомендации для @npmjs:
Документируйте магию ваших команд каждый раз, когда они появляются в документации.
В документации к команде npm install
действительно есть строка, в которой говорится:
npm install <folder>
:
Установите пакет в каталог как символическую ссылку в текущем проекте.
Однако я не смотрел на эту часть документации, исследуя эту проблему. Есть причина, почему.
Причина в том, что проблема не возникла после того, как я запустил npm install
для установки DependencyModule, поэтому не имело смысла смотреть документацию по команде install
. Вместо этого в коде уже был установлен DependencyModule в PrincipalModule; ошибка обнаружилась только после того, как код попытался использовать от _dirname
до app-root-path
.
Вместо этого я просмотрел Документацию по локальным путям на веб-сайте NPM, и там ничего не говорится о наличии символической ссылки для установки локальных пакетов.
Сегодня я узнал: установка NPM в локальные файлы создает символическую ссылку. По умолчанию командная строка zip не содержит символическую ссылку.
Теперь я могу:
- Прекратите использовать
app-root-path
. Вместо этого импортируйте модули относительно того места, где они находятся в файловой системе, независимо от корня проекта [1]. - Используйте
zip --symlink
при архивировании кода для prod с очевидным комментарием, почему [2].
Эта проблема отлично показывает, когда безобидное удобство может обернуться отключением.
Небольшое безобидное удобство может стать существенно вредным неудобством.
А ты, что думаешь по этому поводу? Смогли бы вы предотвратить возникновение этой ошибки?
Рад слышать твои мысли.
1: Проблема здесь в том, что вы должны полагаться на автоматическую настройку относительного пути в среде IDE при создании нового каталога. Другая проблема заключается в том, что Git показывает разницу с изменениями каталога с ../
на ../../
вместо ${root}/src/book-payments/ConnectRepository.js
на ${root}/src/book-payments/storage/ConnectRepository.js
, которые более понятны.
2: Это происходит за счет связывания одной команды, используемой в одной части системы, с командой, используемой в другой. Когда разработчик читает код, нет четкой причинно-следственной связи между каждой частью.
Спасибо Марселю Сильве и Раймо Радчевски за их полезный вклад в этот пост.
Спасибо за прочтение. Если у вас есть отзывы, напишите мне в Twitter, Facebook или Github.