Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Rails Executor

Rails Executor

Rails Executor: the border between application and framework code

Do you know how Ruby on Rails cleans up caches and resources between requests and background jobs and how it knows when and which code to reload?

Rails Executor is a little-known Rails internal detail a typical application developer will never notice or need to know about. However, if you create gems that call an application’s code via callbacks, that knowledge becomes essential.

As an open-source Ruby gem developer, I’ve encountered a few bugs that were resolved using the Rails Executor. For this reason, knowledge of the Rails Executor is essential for developers creating gems that are calling Rails app code. And you’ll get some interesting knowledge about how Rails works along the way!

A talk from Kaigi on Rails 2023 conference.

Andrey Novikov

October 27, 2023
Tweet

More Decks by Andrey Novikov

Other Decks in Programming

Transcript

  1. Rails Executor the border between application and framework code Andrey

    Novikov, Evil Martians Kaigi on Rails 2023 27 October 2023
  2. About me Hi, I’m Andrey (アンドレイ ) Back-end engineer at

    Evil Martians Writing Ruby, Go, and whatever SQL, Dockerfiles, TypeScript, bash… Love open-source software Created and maintaining a few Ruby gems Living in Japan for 1 year already Driving a moped And also a bicycle to get kids to kindergarten
  3. Martian Open Source Yabeda: Ruby application instrumentation framework Lefthook: git

    hooks manager AnyCable: a real-time server for Rails and beyound PostCSS: A tool for transforming CSS with JavaScript Imgproxy: Fast and secure standalone server for resizing and converting remote images A Figma plugin that ensures UI text is readable by leveraging the new APCA algorithm Overmind: Process manager for Procfile-based applications and tmux Even more at evilmartians.com/oss
  4. Index 1. The problem explanation 2. What is Rails Executor

    3. How to use it 4. Real world examples 5. My experience with it
  5. What developer sees class KaigiOnRailsController < ApplicationController # framework code

    executes before action def index # your code here end # framework code executes after action # implicit rendering (your code again) # framework code executes after action end
  6. What code actually executes A long way to serve a

    request! $ rails middleware use ActionDispatch::HostAuthorization use Rack::Sendfile use ActionDispatch::Static use ActionDispatch::Executor # Spoiler alert! use Rack::Runtime use ActionDispatch::RequestId use ActionDispatch::RemoteIp use Rails::Rack::Logger # 18 entries skipped run MyApp::Application.routes
  7. Two worlds Framework code (and gems’ code also) Your application

    code And for basic actions a lot more framework code is executed than your code. It can be compared to a kernel and user space in an operating system. Kernel code manages resources and provides APIs User space code is executed by, and uses APIs provided by kernel
  8. What we take for granted 1. Automatic code reloading in

    development 2. Implicit database connection management 3. Caches cleanup 4. and more…
  9. Code reloading It should be fast or the developer experience

    will be bad. And framework and gems usually doesn’t change at all. So only the application code need to be reloaded.
  10. Resource management We’ve got used not to care about connection

    pools, etc. class KaigiOnRailsController < ApplicationController # framework code executes before action def index record = MyModel.find_by(…) record.update(…) # Database connection? What database connection? end end
  11. Resource management If we had to do it manually: (Thanks

    to all possible gods we don’t have to!) ActiveRecord::Base.connection_pool.with_connection do |conn| end # This is not real code! class KaigiOnRailsController < ApplicationController def index record = MyModel.find_by(…, connection: conn) record.update(…, connection: conn) end end
  12. Why I should care? Usually, you should not! That’s why

    we love Ruby on Rails. And that’s why there is Rails Executor.
  13. But when I should care? When you are writing a

    gem that calls application code. MyGem.do_something_later do # Your callback code here end class KaigiOnRailsController < ApplicationController def index # render something end end
  14. Examples of gems that call application code Background jobs: Sidekiq,

    DelayedJob, GoodJob… Scheduled jobs: Whenever, … Non-HTTP handlers: ActionCable, AnyCable… Custom instrumentation: Yabeda… Messaging: NATS subscriptions, Karafka consumers, …
  15. Rails Executor Or API to call application code from framework

    code Border point between application and framework code
  16. Rails Executor Read more: guides.rubyonrails.org/threading_and_code_execution.html Use it when you need

    to call application code once. Wraps unit of work (action, job, etc.) Is re-entrant safe to call wrap inside of another wrap and from different threads Defines callbacks to_run and to_complete called before and after enclosed block ` ` ` ` Rails guides on threading (日本語 )
  17. Rails Reloader Read more: guides.rubyonrails.org/threading_and_code_execution.html Use it in long running

    processes instead of Executor Wraps unit of work too Calls Executor if needed (its callbacks are executed) Reloads code if changed Adds more callbacks (called only on code reload!) to_prepare , to_run , to_complete , before_class_unload , after_class_unload ` ` ` ` ` ` ` ` ` ` Rails guides on threading (日本語 )
  18. How to integrate with Rails Executor/Reloader Case 1: to call

    application code from a gem code. Wrap the call to application code in Rails.application.executor.wrap or, most often, Rails.application.reloader.wrap : And that’s it! ` ` ` ` Rails.application.reloader.wrap do # call application code here end
  19. How to integrate with Rails Executor/Reloader Case 2: to do

    something before/after every request/job/etc. Register to_run and to_complete callbacks on the application Executor instance: ` ` ` ` Rails.application.executor.to_run do # do something before end Rails.application.executor.to_complete do # do something after end
  20. How to integrate with Rails Executor/Reloader Case 3: to do

    something before/after every code reload. Register to_prepare or other callbacks on the application Reloader instance: ` ` Rails.application.reloader.to_prepare do # do something whe code has been reloaded end
  21. Rails itself uses it too (of course!) ActionCable wraps every

    incoming WebSocket connection into Rails Executor: [RailsWorld 2023] Untangling cables & demystifying twiste [RailsWorld 2023] Untangling cables & demystifying twiste [RailsWorld 2023] Untangling cables & demystifying twiste… … … by by by Vladimir Dementyev Vladimir Dementyev Vladimir Dementyev See RailsWorld 2023: Untangling cables & demystifying twisted transistors Action Cable Executor slide
  22. Typical example: resource management ActionPolicy gem caches authorization rules in

    a per-thread cache. Rails Executor cleans up this cache between requests. See lib/action_policy/railtie.rb:59 initializer "action_policy.clear_per_thread_cache" do |app| app.executor.to_run { ActionPolicy::PerThreadCache.clear_all } app.executor.to_complete { ActionPolicy::PerThreadCache.clear_all end module ActionPolicy class Railtie < ::Rails::Railtie end end ` ` action_policy/railtie.rb:59
  23. Advanced usage: AnyCable messaging batching Less network round-trips, guaranteed order

    of messages. See anycable-rails pull request № 189 executor.to_run do # Start collecting messages instead of sending them immediately AnyCable.broadcast_adapter.start_batching end executor.to_complete do # Send all collected messages at once AnyCable.broadcast_adapter.finish_batching end anycable-rails pull request № 189
  24. Advanced usage: ViewComponent previews Use Reloader to_prepare callback to show

    actual Source Previews of components in development. See view_component pull request № 1147 ` ` app.config.to_prepare do # Clear source code cache on code reload MethodSource.instance_variable_set(:@lines_for_file, {}) end view_component pull request № 1147
  25. There is always room for contribution Not all gems are

    integrated with Rails Executor/Reloader yet.
  26. My contribution: NATS subscriptions NATS is a modern, simple, secure

    and performant message communications system for microservice world. runs in a long-running process executes application code callback multiple times Nice to have automatic code reloading! nats = NATS.connect("demo.nats.io") nats.subscribe("service") do |msg| # Your logic to handle incoming messages end
  27. My contribution: NATS subscriptions Need to wrap subscription message handler

    in Rails.application.reloader.wrap : To allow NATS client to be used outside of Rails, there is framework-agnostic “reloader” (as Sidekiq does). See nats-pure.rb pull request № 120 ` ` -begin +client.reloader.call do callback.call(message) class NATS::Rails < ::Rails::Engine config.after_initialize do NATS::Client.default_reloader = NATS::Rails::Reloader.new end end nats-pure.rb pull request № 120
  28. Up to the next time! Come to Izumo, Shimane, Japan!

    See you at Izumo Ruby meet-up on 2023-11-11! Next day after RubyWorld conference finishes in Matsue. Izumo Ruby meet-up talk announce
  29. Thank you! @Envek @Envek @Envek @Envek github.com/Envek @evilmartians @evilmartians @evilmartians_jp

    (日本語 ) @evil.martians evilmartians.jp Our awesome blog: evilmartians.com/chronicles! @hachi8833さんが作ってくれた日本語の翻訳があります! See these slides at envek.github.io/kaigionrails-rails-executor