We have a Rails app that responds to multiple TLDs, including subdomains. One of those domains is a .co.uk.
domain, therefore the TLD length in that case is 2 (eg: ourapp.es
, ourapp.co.uk
, api.ourapp.es
, api.ourapp.co.uk
.
In order to dynamically change the TLD length we've using this Rack middleware :
class Rack::TldLength
def initialize(app, host_pattern, host_tld_length)
@app = app
@host_pattern = Regexp.new(host_pattern)
@host_tld_length = host_tld_length
end
def call(env)
original_tld_length = tld_length
request = Rack::Request.new(env)
set_tld_length(@host_tld_length) if request.host =~ @host_pattern
@app.call(env)
ensure
set_tld_length(original_tld_length)
end
private
def tld_length
ActionDispatch::Http::URL.tld_length
end
def set_tld_length(length)
ActionDispatch::Http::URL.tld_length = length
end
end
This has been working so far until we decided to migrate from Unicorn to puma . With Unicorn each request would go to a different unicorn worker (process) and there was no problem. However with puma each request can be processed by a different thread. We suspect that changing the value ActionDispatch::Http::URL.tld_length
is not thread safe, but we're struggling to find an alternative to this.
It seems that the Rails routing (where we define routes with subdomain constraints) depends on setting the ActionDispatch::Http::URL.tld_length
properly.
Is there any workaround to keep the concurrency offered by having multiple threads while still being able to handle multiple domains with different TLD lengths?
You state that:
It seems that the Rails routing (where we define routes with subdomain constraints) depends on setting the ActionDispatch::Http::URL.tld_length properly.
It seems to me that the easiest way is to normalize the "HOST"
parameter in the env
to allow for all host names to behave equally.
ie
# Place this middleware at the top of the chain, before any Rails middleware.
class Rack::FixedHost
# a host_pattern can be: /(foo.com|foo.co.uk|foo.bor.co.uk)$/
def initialize(app, host_pattern, normalized_host)
@app = app
@host_pattern = Regexp.new(host_pattern)
@normalized_host = normalized_host
end
def call(env)
env[:ORIGINAL_HOST] = env['HTTP_HOST'.freeze] || @normalized_host
env[:ORIGINAL_DOMAIN] = env[:ORIGINAL_HOST].match(@host_pattern).to_a[0] || @normalized_host
env['HTTP_HOST'.freeze] = env[:ORIGINAL_HOST].to_s.sub(@host_pattern, @normalized_host)
@app.call(env)
end
end
To clarify: normalizing a host means that it always has the same host name postfix, regardless of the original postfix, allowing for easier subdomain extraction.
ie, for sub.foo.com
, sub.foo.co.uk
and sub.foo.bor.co.uk
the normalized_host
will always be sub.foo.com
.
In this example, sub
is easily extracted after the different host variation ( foo.com
, foo.co.uk
and foo.bor.co.uk
) have all been normalized to the single "normalized" variation ( foo.com
).
By default, methods such as url_for
will construct a relative URL, so the actual host name isn't important.
However, if you use url_for
or other functions to provide a complete URL, you might consider using an explicit :host
to direct traffic to the regional host name you're using. ie:
url_for(action: 'index', host: "admin.#{request.env[:ORIGINAL_DOMAIN]}")
This, of course could be made even more powerful by extracting the original domain name before normalizing the host, allowing you to route to specific subdomains while keeping the regional domain.
Note (my original observation / answer):
Your code stores the TLD length of each request in a shared global variable.
When two parallel requests arrive, on two different threads, it is a matter of chance to know which TLD length will be used (the last one written, most probably, if no data "shearing" occurs).
A thread-safe approach will store the information in the env
variable, allowing each request it's own TLD length.
The following example will NOT work, because I don't handle TLD lengths and have no idea how to calculate them... but it shows the use of the env
as a thread-safe per-request storage.
class Rack::TldLength
def initialize(app, host_pattern, host_tld_length)
@app = app
@host_pattern = Regexp.new(host_pattern)
@default_tld_length = host_tld_length
end
def call(env)
# ActionDispatch::Http::URL.tld_length = @default_tld_length if(env["HTTP_HOST".freeze].to_s =~ @host_pattern)
env[:hosts_tld] = (env["HTTP_HOST".freeze].to_s =~ @host_pattern) ? @default_tld_length : ActionDispatch::Http::URL.tld_length
@app.call(env)
end
end
ActionDispatch::Http::URL
stores tld_length
as a module variable , which is to say as a single global variable for your whole application. There is no way to make that thread safe. I suspect the design thinking was that your app would be at just one domain and so one global setting, set at startup, would be sufficient, so it was not necessary to make tld_length
thread safe.
ActionDispatch is pretty central to Rails, so I would try to avoid mucking with it. How hard would it be to run 2 Puma servers and send all tld_length = 2
traffic to one server and tld_length = 1
to the other server? If you are running a server farm, that would be a reasonable sharding key and would keep you from having to do any further tricks.
If I had to run it in 1 server, I would look into modifying ActionDispatch::Http::URL
so that it stores tld_length
in a Thread-local variable instead of a module variable and set it on each request. You would also have to change the functions that use the module variable as a default value, like domain
to use the thread variable as the default, which might be easiest by using an accessor function.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.