Rails Reload Dynamic Routes on Multiple Instances/Servers

Rails reload dynamic routes on multiple instances/servers

We finally found a solution that works pretty well and is also not affecting performance too much. We use the fact that Threads in production are keeping states across requests. So we decided to create a middleware that checks the latest timestamp of a routes change and in case the timestamp is not the same as the one saved in Thread.current we force a Frontend::Application.reload_routes!

config/production.rb

Frontend::Application.configure do
...
config.middleware.use RoutesReloader
...
end

app/middleware/routes_reloader.rb

class RoutesReloader
SKIPPED_PATHS = ['/assets/', '/admin/']

def initialize(app)
@app = app
end

def call(env)
if reload_required?(env)
timestamp = Rails.cache.read(:routes_changed_timestamp)

if Thread.current[:routes_changed_timestamp] != timestamp
Frontend::Application.reload_routes!

Thread.current[:routes_changed_timestamp] = timestamp
end
end

@app.call(env)
end

private

def reload_required?(env)
SKIPPED_PATHS.none? { |word| env['PATH_INFO'].include?(word) }
end
end

app/model/routes.rb

class Routes < ActiveRecord::Base

after_save :save_timestamp

private

def save_timestamp
ts = Time.zone.now.to_i
Rails.cache.write(:routes_changed_timestamp, ts, expires_in: 30.minutes)
end
end

Benefits:

  • You can exclude the reload on certain paths like /assets/ and /admin/
  • Threads server multiple requests and the reload only happens once
  • You can implement this on any model you like

Caveats:

  • New Threads will load routes twice
  • All Threads will reload routes if you clear Rails Cache (you could overcome this with a persistent solution; e.g. saving the timestamp into mysql and then into cache)

But overall we didn't recognise any performance drops.

We have been struggling with this now for years and the above solution is the first that really helped us reloading routes on multiple threads.

How do I refresh routes for a ruby on rails app that has 2 servers?

spickermann has it right (especially the XY Problem comment). I just wanted to mention you could also do:

namespace :templates do
scope :pdfs do
get '*shortcode', to: 'pdfs#show', as: :pdf
end
end

Which gives you:

templates_pdf GET    /templates/pdfs/*shortcode(.:format)   templates/pdfs#show

'*shortcode' is a wildcard matcher. So, if you have a URL like:

localhost:3000/templates/pdfs/a-cool-template

It will route to templates/pdfs#show with a params[:shortcode] == 'a-cool-template'.

Then, your Templates::PdfsController might look something like:

class Templates::PdfsController < ApplicationController

def show
begin
render params[:shortcode]
rescue MissingTemplateError
# do something here
end
end

end

If app/views/templates/pdfs/a-cool-template.html.erb exists, it will be rendered. If it doesn't, then you catch the MissingTemplate error and respond accordingly. (BTW, I forget what the actual name of the MissingTemplate error is, so what I have there is probably not correct.)

If you have multiple template types, you could do:

namespace :templates do
[:pdfs, :html].each do |template_type|
scope template_type do
get '*shortcode', to: "#{template_type}#show", as: template_type
end
end
end

Which gives you:

templates_pdfs GET    /templates/pdfs/*shortcode(.:format)  templates/pdfs#show
templates_html GET /templates/html/*shortcode(.:format) templates/html#show

Is it a bad idea to reload routes dynamically in Rails?

Quick Solution

Have a catch-all route at the bottom of routes.rb. Implement any alias lookup logic you want in the action that route routes you to.

In my implementation, I have a table which maps defined URLs to a controller, action, and parameter hash. I just pluck them out of the database, then call the appropriate action and then try to render the default template for the action. If the action already rendered something, that throws a DoubleRenderError, which I catch and ignore.

You can extend this technique to be as complicated as you want, although as it gets more complicated it makes more sense to implement it by tweaking either your routes or the Rails default routing logic rather than by essentially reimplementing all the routing logic yourself.

If you don't find an alias, you can throw the 404 or 500 error as you deem appropriate.

Stuff to keep in mind:

Caching: Not knowing your URLs a priori can make page caching an absolute bear. Remember, it caches based on the URI supplied, NOT on the url_for (:action_you_actually_executed). This means that if you alias

/foo_action/bar_method

to

/some-wonderful-alias

you'll get some-wonderful-alias.html living in your cache directory. And when you try to sweep foo's bar, you won't sweep that file unless you specify it explicitly.

Fault Tolerance: Check to make sure someone doesn't accidentally alias over an existing route. You can do this trivially by forcing all aliases into a "directory" which is known to not otherwise be routable (in which case, the alias being textually unique is enough to make sure they never collide), but that isn't a maximally desirable solution for a few of the applications I can think of of this.

how to reload routes /config/routes/* in rails 4?

You can use:

Rails.application.reload_routes!

You can read about it here (will have to use find)

Rails 4.2: How to forcefully reload (engine's) route?

config.paths["config/routes.rb"] << YOUR_ROUTE_FILE

Rails 4 url_for with host constraint

I have had the same task in my application. url_for ignores host param. But we could create additional path helpers in our ApplicationController in the following way:

ApplicationController.rb

%w( shop_show ).each do |helper|
helper_name = "#{helper}_path".to_sym
helper_method helper_name
define_method(helper_name) { |*args| send "#{helper}_#{site.id}_path", *args }
end

After that you are able to use universal path shop_show_path in your views. Of course, you should dynamically assign site variable depending on your host/domain.



Related Topics



Leave a reply



Submit