Slide 1

Slide 1 text

Rails Executor the border between application and framework code Andrey Novikov, Evil Martians Kaigi on Rails 2023 27 October 2023

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

evilmartians.com 🇯🇵 evilmartians.jp 🇯🇵 邪悪な火星人? イービルマーシャンズ!

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

Index 1. The problem explanation 2. What is Rails Executor 3. How to use it 4. Real world examples 5. My experience with it

Slide 6

Slide 6 text

The border Why we need to distinguish between application and framework code?

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

What we take for granted 1. Automatic code reloading in development 2. Implicit database connection management 3. Caches cleanup 4. and more…

Slide 11

Slide 11 text

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.

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Why I should care? Usually, you should not! That’s why we love Ruby on Rails. And that’s why there is Rails Executor.

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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, …

Slide 17

Slide 17 text

Rails Executor Or API to call application code from framework code Border point between application and framework code

Slide 18

Slide 18 text

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 (日本語 )

Slide 19

Slide 19 text

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 (日本語 )

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

So all aforementioned gems are doing it? YES!

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

There is always room for contribution Not all gems are integrated with Rails Executor/Reloader yet.

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

That’s it!

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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