$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  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

    View Slide

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

    View Slide

  4. 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

    View Slide

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

    View Slide

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

    View Slide

  7. 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

    View Slide

  8. 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

    View Slide

  9. 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

    View Slide

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

    View Slide

  11. 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.

    View Slide

  12. 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

    View Slide

  13. 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

    View Slide

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

    View Slide

  15. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  20. 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

    View Slide

  21. 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

    View Slide

  22. 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

    View Slide

  23. So all aforementioned gems are doing it?
    YES!

    View Slide

  24. 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

    View Slide

  25. 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

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

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

    View Slide

  29. 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

    View Slide

  30. 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

    View Slide

  31. That’s it!

    View Slide

  32. 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

    View Slide

  33. 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

    View Slide