Rail 5 swap ActionDispatch::Reloader with a custom reloader - ruby-on-rails

We have a use case for mounting a mock engine to process sessions when developing locally in which a custom session middleware calls the mock engine via Net::http request when a request comes through.
When there is code change, the reloader is triggered and here calls the ActiveSupport::Dependencies to start unloading. Then the request is pass down to our custom session middleware and the http request is fired.
However since the http request calls to a mountable engine, it goes thought the same middlewares again and the reloader unloads all the dependencies again which cause the first reload to timeout. So the goal is to be able to skip the reload for the second request.
I added the follow code to ActionDispatch::Reloader here and it does exactly what I wanted.
class Reloader < Executor
def initialize(app, executor)
super(app, executor)
end
def call(env)
request = ActionDispatch::Request.new(env)
return #app.call(env) if skip_request?(request)
super(env)
end
def skip_request?(request)
request.path_info.start_with?('/session')
end
end
Then I want to make this cleaner figured to pull that out completely to a module and just do a swap like this from an initializer
app.config.middleware.swap(::ActionDispatch::Reloader, MyModule::CustomReloaderMiddleware)
Here is the module
require 'action_dispatch'
module MyModule
class CustomReloaderMiddleware < ActionDispatch::Executor
def initialize(app, executor)
#app, #executor = app, executor
end
def call(env)
request = ActionDispatch::Request.new(env)
return #app.call(env) if skip_request?(request)
super(env)
end
def skip_request?(request)
request.path_info.start_with?('/session')
end
end
end
But I ran into a couple issues.
Uncaught exception: wrong number of arguments (given 1, expected 2) from for the initialize in MyModule, when I starts the server. Then I tried the following
#1
def initialize(app, executor = nil)
#app, #executor = app, executor
end
#2
def initialize(app, executor = nil)
#app, #executor = app, ActiveSupport::Reloader
end
Both of them starts the service correctly and I see the request going through this middleware but it does not reload the code.. So I wondered what is the correct way of swapping ActionDispatch::Reloader with a custom reloader ?

You need to pass your middleware's additional argument to the swap call:
app.config.middleware.swap(::ActionDispatch::Reloader, MyModule::CustomReloaderMiddleware, app.reloader)
That's the same argument given when ActionDispatch::Reloader is first added -- it's the application's reloader, which is a more specifically configured subclass of AS::Reloader (so you were on the right track).

Related

How do I create delayed_job jobs with hooks/callbacks?

I am using the most basic version of delayed_job in a Rails app. I have the max time allowed for a delayed_job set at 10 minutes. I would like to get the hooks/callbacks working so I can do something after a job stop executing at the 10 minute mark.
I have this set in my rails app:
config.active_job.queue_adapter = :delayed_job
This is how I normally queue a job:
object.delay.object_action
The hook/callback example is for a named job but the basic, getting started steps are not for a named job. So I don't think I have a named job. Here is the example given to get the callbacks working:
class ParanoidNewsletterJob < NewsletterJob
def enqueue(job)
record_stat 'newsletter_job/enqueue'
end
def perform
emails.each { |e| NewsletterMailer.deliver_text_to_email(text, e) }
end
def before(job)
record_stat 'newsletter_job/start'
end
def after(job)
record_stat 'newsletter_job/after'
end
def success(job)
record_stat 'newsletter_job/success'
end
def error(job, exception)
Airbrake.notify(exception)
end
def failure(job)
page_sysadmin_in_the_middle_of_the_night
end
end
I would love to get the after or error hooks/callbacks to fire.
Where do I put these callbacks in my Rails app to have them fire for the basic delayed_job setup? If I should be using ActiveJob callbacks where do you put those callbacks given delayed_job is being used?
You cannot use object.delay.object_action convenience syntax if you want more advanced features like callbacks. The #delay convenience method will generate a job object that works similar to this:
# something like this is already defined in delayed_job
class MethodCallerJob
def initialize(object, method, *args)
#object = object
#method = method
#args = args
end
def perform
#object.send(#method, *#args)
end
end
# `object.delay.object_action` does the below automatically for you
# instantiates a job with your object and method call
job = MethodCallerJob.new(object, :object_action, [])
Delayed::Job.enqueue(job) # enqueues it for running later
then later, in the job worker, something like the below happens:
job = Delayed::Job.find(job_id) # whatever the id turned out to be
job.invoke_job # does all the things, including calling #perform and run any hooks
job.delete # if it was successful
You have to create what the delayed_job README calls "Custom Jobs", which are just plain POROs that have #perform defined at a minimum. Then you can customize it and add all the extra methods that delayed_job uses for extra features like max_run_time, queue_name, and the ones you want to use: callbacks & hooks.
Sidenote: The above info is for using delayed_job directly. All of the above is possible using ActiveJob as well. You just have to do it the ActiveJob way by reading the documentation & guides on how, just as I've linked you to the delayed_job README, above.
You can create delayed_job hooks/callback by something like this
module Delayed
module Plugins
class TestHooks < Delayed::Plugin
callbacks do |lifecycle|
lifecycle.before(:perform) do |_worker, job|
.....
end
end
end
end
end
And need this plugin to initializer
config/initializers/delayed_job.rb
require_relative 'path_to_test_plugin'
Delayed::Worker.plugins << Delayed::Plugins::TestHooks
Similar to perform there are also hooks for success failure and error.
And similar to 'before' you can also capture the 'after' hooks.

Default arguments in sidekiq perform_async

I need to access the current_user in sidekiq jobs. One way to do this is pass the current_user in the perform_async, however since it's needed at many places I wanted to add it by default.
So I added a server middleware which would fetch the current_user login and set it in Thread.current, here's my middleware
class SidekiqServerMiddleware
def call(_worker, job, _queue)
set_current_user(job['args'])
yield
end
private
def set_current_user(args)
last_arg = args.last
return unless last_arg.is_a?(Hash) && last_arg.key?('current_user')
RequestStore.store[:current_user] = last_arg['current_user']
end
end
I am using RequestStore to set Thread.current in thread safe way.
However right now I've to pass current_user in the perform_async call. I was searching for a way for sidekiq to do this out of the box but couldn't find any.
One way to achieve this is to create custom module for Sidekiq::Worker
module CustomSidekiqWorker
include Sidekiq::Worker
before_action set_current_user, only: :perform_async
def set_current_user
RequestStore.store[:current_user] = current_user.login
end
end
And then instead of including Sidekiq::Worker, include CustomSidekiqWorker. Is there a more neat way to do this ? Maybe some ruby meta programming I am missing. Thanks for the help.
You need a client/server middleware pair, not just server-side.
https://github.com/mperham/sidekiq/wiki/Middleware

How can I get a returned value from a Thread in my scenario?

So I'm wanting to parse a table on about 10 websites, so I want to create a new thread for each site. However, I'm not exactly sure how to return the data from this type of request.
Here's one class:
class TestRequest
def initialize
end
def start
urls = ['site1','site2','site3']
existing_data = Data.pluck(:symbol, :page)
data = GetData.pool(size: 10)
urls.each do |url|
data.async.perform_requests(url, existing_data)
end
end
end
and then GetData class looks like this:
require 'celluloid/current'
class GetData
include Celluloid
def perform_requests(url, existing_data)
# perform HTTP request
# parse HTTP response
# return returned data ???
end
end
What I'd ultimately like to do is have an instance variable in TestRequest class and simply add the returned value from GetData into that instance variable from the TestRequest class. After the threads are finished, I want to perform another action using the data in the instance variable.
I tried playing around with attr_reader, but it doesn't seem to play in my favor.
I tried this:
class TestRequest
def initialize
end
def start
#returned_data = []
urls = ['site1','site2','site3']
existing_data = Data.pluck(:symbol, :page)
data = GetData.pool(size: 10)
urls.each do |url|
data.async.perform_requests(url, existing_data)
end
end
attr_reader :returned_data
end
and then
require 'celluloid/current'
class GetData
include Celluloid
def perform_requests(tr, existing_data)
# perform HTTP request
# parse HTTP response
t = TestData.new
t.returned_data << "value"
end
end
but this doesn't work either.
Multi-threading and Ruby on Rails don't mix very well.
However, you should consider using the ActiveJob documentation (http://guides.rubyonrails.org/active_job_basics.html).
With ActiveJob, you can enqueue jobs and have them executed in the background. There are hooks method defined as well to notify you when a job is about to start, is running or have finished.

Reloading rails middleware without restarting the server in development

I have a rails 4 app with middleware located at lib/some/middleware.rb which is currently injected into the stack though an initializer like so:
MyApp::Application.configure.do |config|
config.middleware.use 'Some::Middleware'
end
Unfortunately, any time I change something I need to restart the server. How can I reload it on each request in development mode? I've seen similar questions about reloading lib code with either autoloading or wrapping code in a to_prepare block but I'm unsure how that could be applied in this scenario.
Thanks,
- FJM
Update #1
If I try to delete the middleware and then re-add it in a to_prepare block I get an error "Can't modify frozen array".
I thought that at some point Rails was smart enough replacing middleware code at runtime, but I may be wrong.
Here is what I came up with, circumventing Ruby class loading craziness and leveraging Rails class reloading.
Add the middleware to the stack:
# config/environments/development.rb
[...]
config.middleware.use "SomeMiddleware", "some_additional_paramter"
Make use of auto-reloading, but make sure that the running rails instance and the already initialized middleware object keep "forgetting" about the actual code that is executed:
# app/middlewares/some_middleware.rb
class SomeMiddleware
def initialize(*args)
#args = args
end
def call(env)
"#{self.class}::Logic".constantize.new(*#args).call(env)
end
class Logic
def initialize(app, additional)
#app = app
#additional = additional
end
def call(env)
[magic]
#app.call(env)
end
end
end
Changes in Logic should be picked up by rails auto reloading on each request.
I think that this actually might become a useful gem!
Building up on #phoet's answer we can actually wrap any middleware with this kind of lazy loading, which I found even more useful:
class ReloadableMiddleware
def initialize(app, middleware_module_name, *middleware_args)
#app = app
#name = middleware_module_name
#args = middleware_args
end
def call(env)
# Lazily initialize the middleware item and call it immediately
#name.constantize.new(#app, *#args).call(env)
end
end
It can be then hooked into the Rails config with any other middleware as its first argument, given as a string:
Rails.application.config.middleware.use ReloadableMiddleware, 'YourMiddleware'
Alternatively - I packaged it into a gem called reloadable_middleware, which can be used like so:
Rails.application.config.middleware.use ReloadableMiddleware.wrap(YourMiddleware)
In Rails 6 with the new default Zeitwork code loader, this works for me:
# at the top of config/application.rb, after Bundler.require
# Load the middleware. It will later be hot-reloaded in config.to_prepare
Dir["./app/middleware/*.rb"].each do |middleware|
load middleware
end
Below it in the section that configures your class Application, add hot-reloading in config.to_prepare:
middleware = "#{Rails.root}/app/middleware"
Rails.autoloaders.main.ignore(middleware)
# Run before every request in development mode, or before the first request in production
config.to_prepare do
Dir.glob("#{middleware}/*.rb").each do |middleware|
load middleware
end
end
Can you not simply use shotgun? If I understand your question you want to ensure the environment reloads on every change you make to your code. That is what shotgun will do.

Rails run code before every request before application.rb

I want to run a piece of code in rails app before every request. Also, it should run before even reaching application_controller.rb.
I know that we can put such stuff in config/initializers or application.rb. But, I want to run this before every request.
Sounds like a job for Rack middleware. You can checkout the Rails on Rack Guide and the RailsCast for details.
So put something like the following in lib:
#lib/my_app_middleware.rb
class MyAppMiddleware
def initialize(app)
#app = app
end
def call(env)
# place the code that you want executed on every request here
end
end
And the following in config/application.rb to enable the middleware
config.middleware.use MyAppMiddleware
Check that its inserted ok:
rake middleware
Thats it!
You'll want to write some Rack Middleware. It's easy to do, here's a simple example which gets the subdomain for the purpose of multi tenant scoping:
class ClientSetup
def initialize(app)
#app = app
end
def call(env)
#request = Rack::Request.new(env)
Multitenant.current_tenant = Tenant.find_by_subdomain!(get_subdomain)
#app.call(env)
end
private
def get_subdomain
host = #request.host
return nil unless !(host.nil? || /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.match(host))
subdomain = host.split('.')[0..-3].first
return subdomain unless subdomain == "www"
return host.split('.')[0..-3][1]
end
end
There's loads more examples around. You then need to add this class to your middleware stack with:
config.middleware.use 'ClientSetup'
in your application.rb.
It's usually a subclass of ApplicationController that gets called when routing dispatches to one of your actions. That being said, if you really want to execute code before the controller is even called (before the before_filters ... etc) then you can modify the chain of middlewares in Rails like so:
config.middleware.insert_after(Rails::Rack::Logger, MyCustomMiddlewareClass)
You can read here for more info: http://guides.rubyonrails.org/rails_on_rack.html#action-dispatcher-middleware-stack.
The example above may change depending on what you are trying to do.

Resources