Треды и выполнение кода в Rails
После прочтения этого руководства вы узнаете: где найти конкурентное выполнение кода в Rails, как интегрировать ручную конкурентность с Rails, как обернуть код приложения с помощью Rails Executor и как повлиять на перезагрузку приложения.
После прочтения этого руководства вы узнаете:
- Где найти конкурентное выполнение кода в Rails
- Как интегрировать ручную конкурентность с Rails
- Как обернуть код приложения с помощью Rails Executor
- Как повлиять на перезагрузку приложения
Автоматическая конкурентность в Rails
Rails автоматически позволяет выполнять различные операции одновременно (конкурентно) для того, чтобы приложение работало эффективнее. В этом разделе мы рассмотрим некоторые из способов, как это происходит за кадром.
При использовании тредового веб-сервера, такого как дефолтный Puma, несколько HTTP-запросов будут обслуживаться одновременно, причём каждому запросу предоставляется свой собственный экземпляр контроллера.
Тредовые адаптеры Active Job, в том числе встроенный адаптер Async, будут также выполнять несколько заданий одновременно. Аналогичным образом управляются каналы Action Cable.
Асинхронные запросы Active Record также выполняются в фоне, позволяя другим процессам работать в основном треде.
Все эти механизмы связаны с несколькими тредами, каждый из которых управляет работой с уникальным экземпляром некоторого объекта (контроллером, заданием, каналом), разделяя глобальное пространство процесса (например, классы и их конфигурации, и глобальные переменные). Пока ваш код не модифицирует ни один из этих общих ресурсов, он может в основном игнорировать существование других тредов.
В остальной части этого руководства описываются механизмы Rails, используемые чтобы сделать другие треды "по большей части игнорируемыми", и как расширения и приложения с особыми требованиями могут использовать эти механизмы.
NOTE: Вы можете прочитать больше о том, как настроить конкурентность Rails, в разделе Поведение фреймворка.
Треды и файберы
Значение active_support.isolation_level в вашем файле config/application.rb даёт возможность определить, где должно храниться внутреннее состояние Rails во время выполнения задач. Если вы используете сервер или обработчик заданий на основе файберов (например, falcon), вам следует установить это значение в :fiber, в противном случае лучше установить его в :thread.
Оборачивание кода приложения
Rails Executor
Rails Executor отделяет код приложения от кода фреймворка, оборачивая написанный вами код, что необходимо при использовании тредов.
Колбэки
Executor состоит из двух колбэков: to_run и to_complete. Колбэк to_run вызывается перед кодом приложения, а to_complete — после.
В приложении Rails по умолчанию колбэки Rails Executor используются для:
- Отслеживания того, какие треды находятся в безопасных позициях для автозагрузки и перезагрузки.
- Включения и отключения кэша запросов Active Record.
- Возврата захваченных подключений Active Record в пул.
- Ограничения времени жизни внутренних кэшей.
Выполнение кода
Если вы пишете библиотеку или компонент, которые будут вызывать код приложения, вы должны обернуть это вызовом Executor:
Rails.application.executor.wrap do
# вызов кода приложения здесь
endTIP: Если вы повторно вызываете код приложения из долгоживущего процесса, возможно, вместо этого вы захотите обернуть его с помощью Reloader.
Каждый тред должен быть обёрнут перед запуском кода приложения, поэтому если ваше приложение вручную делегирует работу другим тредам, например, через Thread.new или функции Concurrent Ruby, использующие пулы тредов, вам следует немедленно обернуть блок:
Thread.new do
# никакого кода здесь
Rails.application.executor.wrap do
# ваш код здесь
end
endNOTE: Concurrent Ruby использует ThreadPoolExecutor, который иногда настраивается опцией executor. Несмотря на название, он не связан с Rails Executor.
Если непрактично оборачивать код приложения в блок (например, Rack API делает это проблематично), вы также можете использовать пару run! / complete!:
Thread.new do
execution_context = Rails.application.executor.run!
# ваш код здесь
ensure
execution_context.complete! if execution_context
endРежим выполнения
Executor поместит текущий тред в режим running в Load Interlock. Эта операция временно заблокируется, если другой тред в данный момент либо автозагружает константу, либо выгружает/перезагружает приложение.
Reloader
Как и Executor, Reloader также оборачивает код приложения. Reloader подходит только там, где долгоживущий процесс на уровне фреймворка повторно вызывает код приложения, например, для веб-сервера или очереди заданий.
NOTE: Rails автоматически оборачивает веб-запросы и воркеры Active Job, поэтому вам редко придётся вызывать Reloader самостоятельно. Всегда учитывайте, не подходит ли Executor лучше для вашего случая.
Если Executor ещё не активен в текущем треде, Reloader вызовет его для вас, поэтому вам нужно вызвать только один. Это также гарантирует, что всё, что делает Reloader, включая его колбэки, происходит обёрнутым внутри Executor.
Rails.application.reloader.wrap do
# вызов кода приложения здесь
endКолбэки
Перед входом в обёрнутый блок Reloader проверит, нужно ли перезагружать запущенное приложение (например, потому что исходный файл модели был модифицирован). Если он определит, что требуется перезагрузка, он подождёт, пока это будет безопасно, а затем выполнит её перед продолжением. Когда приложение настроено на постоянную перезагрузку независимо от того, обнаружены ли какие-либо изменения, перезагрузка вместо этого выполняется в конце блока.
Reloader также предоставляет колбэки to_run и to_complete; они вызываются в тех же точках, что и колбэки Executor, но только когда текущее выполнение инициировало перезагрузку приложения. Когда перезагрузка не считается необходимой, Reloader вызовет обёрнутый блок без других колбэков.
Выгрузка классов
Наиболее значимая часть процесса перезагрузки — это "выгрузка классов", когда все автозагруженные классы удаляются, готовые к повторной загрузке. Это происходит непосредственно перед колбэком to_run или to_complete, в зависимости от настройки reload_classes_only_on_change.
Часто дополнительные действия по перезагрузке необходимо выполнить либо непосредственно перед, либо сразу после "выгрузки классов", поэтому Reloader также предоставляет колбэки before_class_unload и after_class_unload.
Конкурентность
Только долгоживущие "верхнеуровневые" процессы должны вызывать Reloader, потому что если он определит, что требуется перезагрузка, он заблокируется, пока все другие треды не завершат любые вызовы Executor.
Если бы это произошло в "дочернем" треде с ожидающим родителем внутри Executor, это вызвало бы неизбежный дедлок: перезагрузка должна произойти до того, как дочерний тред будет выполнен, но она не может быть безопасно выполнена, пока родительский тред находится в середине выполнения. Дочерние треды должны вместо этого использовать Executor.
Поведение фреймворка
Компоненты фреймворка Rails используют Executor и Reloader для управления собственными потребностями в конкурентности.
ActionDispatch::Executor и ActionDispatch::Reloader — это Rack middleware, которые оборачивают запросы предоставленным Executor или Reloader соответственно. Они автоматически включены в дефолтный стек приложения. Reloader гарантирует, что любой входящий HTTP-запрос будет обслужен свежезагруженной копией приложения, если произошли какие-либо изменения кода.
Active Job также оборачивает выполнение своих заданий с помощью Reloader, загружая последний код для выполнения каждого задания, когда оно выходит из очереди.
Action Cable вместо этого использует Executor: поскольку соединение Cable связано с конкретным экземпляром класса, перезагружать для каждого входящего сообщения WebSocket невозможно. Однако оборачивается только обработчик сообщений; долгоживущее соединение Cable не предотвращает перезагрузку, вызванную новым входящим запросом или заданием. Вместо этого Action Cable использует колбэк before_class_unload Reloader для отключения всех своих соединений. Когда клиент автоматически переподключается, он будет взаимодействовать с новой версией кода.
Перечисленные выше — это точки входа во фреймворк, поэтому они отвечают за обеспечение защиты своих соответствующих тредов и принятие решения о необходимости перезагрузки. Другие компоненты должны использовать Executor только при порождении дополнительных тредов.
Конфигурация Reloader и Executor
Reloader проверяет изменения файлов только тогда, когда config.enable_reloading и config.reload_classes_only_on_change оба равны true. Это значения по умолчанию в окружении development.
Когда config.enable_reloading равно false (в production по умолчанию), Reloader — это лишь проброс к Executor.
У Executor всегда есть важная работа, например управление подключениями к базе данных. Когда config.enable_reloading равно false, а config.eager_load равно true (значения по умолчанию для production), перезагрузка не будет происходить, поэтому Load Interlock ему не нужен. С настройками по умолчанию в окружении development Executor будет использовать Load Interlock, чтобы гарантировать загрузку констант только когда это безопасно.
Load Interlock
Reloading Interlock гарантирует, что перезагрузка кода может выполняться безопасно в многотредовом окружении выполнения.
Безопасно выполнять выгрузку/перезагрузку только когда код приложения не находится в середине выполнения: после перезагрузки константа User, например, может указывать на другой класс. Без этого правила несвоевременная перезагрузка означала бы, что User.new.class == User или даже User == User могут быть false.
Reloading Interlock устраняет это ограничение, отслеживая, какие треды в данный момент выполняют код приложения, и гарантируя, что перезагрузка ожидает, пока ни один другой тред не выполняет код приложения.
permit_concurrent_loads
Executor автоматически захватывает блокировку running на время своего блока, и автозагрузка знает, когда обновляться до блокировки load, а затем переключаться обратно на running.
Однако другие блокирующие операции, выполняемые внутри блока Executor (включая весь код приложения), могут без необходимости удерживать блокировку running. Если другой тред встречает константу, которую он должен автозагрузить, это может вызвать дедлок.
Например, предположив, что User ещё не загружен, следующее приведёт к дедлоку:
Rails.application.executor.wrap do
th = Thread.new do
Rails.application.executor.wrap do
User # внутренний тред ждёт здесь; он не может загрузить
# User, пока другой тред выполняется
end
end
th.join # внешний тред ждёт здесь, удерживая блокировку 'running'
endЧтобы предотвратить этот дедлок, внешний тред может вызвать permit_concurrent_loads. Вызывая этот метод, тред гарантирует, что не будет разыменовывать ни одну потенциально автозагружаемую константу внутри предоставленного блока. Самый безопасный способ выполнить это обещание — поместить его как можно ближе к блокирующему вызову:
Rails.application.executor.wrap do
th = Thread.new do
Rails.application.executor.wrap do
User # внутренний тред может захватить блокировку 'load',
# загрузить User и продолжить
end
end
ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
th.join # внешний тред ждёт здесь, но не имеет блокировки
end
endДругой пример с использованием Concurrent Ruby:
Rails.application.executor.wrap do
futures = 3.times.collect do |i|
Concurrent::Promises.future do
Rails.application.executor.wrap do
# здесь делаем работу
end
end
end
values = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
futures.collect(&:value)
end
endActionDispatch::DebugLocks
Если ваше приложение попадает в дедлок и вы думаете, что в этом может быть замешан Load Interlock, вы можете временно добавить middleware ActionDispatch::DebugLocks в config/application.rb:
config.middleware.insert_before ActionDispatch::Executor,
ActionDispatch::DebugLocksЕсли затем перезапустить приложение и снова вызвать условие дедлока, /rails/locks покажет сводку всех тредов, известных в данный момент interlock, какой уровень блокировки они удерживают или ждут, и их текущий бэктрейс.
Обычно дедлок будет вызван interlock, конфликтующим с какой-либо внешней блокировкой или блокирующим вызовом ввода-вывода. Найдя его, вы можете обернуть его с помощью permit_concurrent_loads.
Создание и настройка генераторов и шаблонов Rails
Генераторы Rails - необходимый инструмент, для улучшения своего рабочего процесса. С помощью этого руководства вы изучите, как создавать генераторы и настраивать существующие.
Использование Rails для API-приложений
В этом руководстве вы узнаете, что предоставляет Rails для API-приложений, как настроить Rails для работы без браузерных функций, какие middleware и модули контроллера выбрать.