Небольшое безобидное удобство может стать существенно вредным неудобством.

Я работал над веб-сервером 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>:

Установите пакет в каталог как символическую ссылку в текущем проекте.

- https://docs.npmjs.com/cli/v7/commands/npm-install

Однако я не смотрел на эту часть документации, исследуя эту проблему. Есть причина, почему.

Причина в том, что проблема не возникла после того, как я запустил npm install для установки DependencyModule, поэтому не имело смысла смотреть документацию по команде install. Вместо этого в коде уже был установлен DependencyModule в PrincipalModule; ошибка обнаружилась только после того, как код попытался использовать от _dirname до app-root-path.

Вместо этого я просмотрел Документацию по локальным путям на веб-сайте NPM, и там ничего не говорится о наличии символической ссылки для установки локальных пакетов.

Сегодня я узнал: установка NPM в локальные файлы создает символическую ссылку. По умолчанию командная строка zip не содержит символическую ссылку.

Теперь я могу:

  1. Прекратите использовать app-root-path. Вместо этого импортируйте модули относительно того места, где они находятся в файловой системе, независимо от корня проекта [1].
  2. Используйте zip --symlink при архивировании кода для prod с очевидным комментарием, почему [2].

Эта проблема отлично показывает, когда безобидное удобство может обернуться отключением.

Небольшое безобидное удобство может стать существенно вредным неудобством.

А ты, что думаешь по этому поводу? Смогли бы вы предотвратить возникновение этой ошибки?

Рад слышать твои мысли.

1: Проблема здесь в том, что вы должны полагаться на автоматическую настройку относительного пути в среде IDE при создании нового каталога. Другая проблема заключается в том, что Git показывает разницу с изменениями каталога с ../ на ../../ вместо ${root}/src/book-payments/ConnectRepository.js на ${root}/src/book-payments/storage/ConnectRepository.js, которые более понятны.

2: Это происходит за счет связывания одной команды, используемой в одной части системы, с командой, используемой в другой. Когда разработчик читает код, нет четкой причинно-следственной связи между каждой частью.

Спасибо Марселю Сильве и Раймо Радчевски за их полезный вклад в этот пост.

Спасибо за прочтение. Если у вас есть отзывы, напишите мне в Twitter, Facebook или Github.