Fixes #39208 - Cache global registration script in smart-proxy - #935
Fixes #39208 - Cache global registration script in smart-proxy#935pablomh wants to merge 1 commit into
Conversation
83ee924 to
bb29755
Compare
ekohl
left a comment
There was a problem hiding this comment.
In Ruby you can pass blocks and that creates a nice abstraction mechanism. In somewhat pseudo-code:
CACHE = {}
MUTEX = Mutex.new
def cache(key, &block)
if (value = CACHE[key])
value
else
MUTEX.synchronize do
CACHE[key] = block.call
end
end
end
def get_value(query)
cache(query) do
real_call_here(query)
end
endYou may need to do something like raising an exception to handle errors from Foreman code (in handle_response) so it doesn't get cached.
527a20b to
0c8e483
Compare
|
Thanks — we followed exactly this pattern. The def cache(key, &block)
value = read_registration_cache(key)
return value if value
self.class.key_mutex(key).synchronize do
value = read_registration_cache(key)
return value if value
result = yield
self.class.registration_script_cache[key] = { body: result, at: Time.now }
self.class.evict_key_mutex(key)
result
end
endThe caller: cache(cache_key) do
response = Proxy::Registration::ProxyRequest.new.global_register(request)
raise ScriptFetchError, response unless response.code == '200'
response.body
endNon-200 responses raise |
|
I pushed a follow-up refinement on top of this branch. Main changes:
The main motivation was that two requests with the same query string but different auth can produce different rendered scripts, since Foreman embeds a user-derived token in the registration script. With a query-only cache key, those could incorrectly share a cache entry. For the auth partitioning, I currently used the raw
Happy to switch that part if there’s a preference. |
ce4e364 to
55c89f4
Compare
55c89f4 to
93afea4
Compare
93afea4 to
d741ddb
Compare
|
Ping? |
Cache the global registration script (GET /register) in an in-memory Concurrent::Map with a 5-minute TTL. The script is identical for all hosts sharing the same registration parameters and auth context, making it safe to serve from cache during concurrent bulk registration. Per-key double-checked locking prevents thundering herd while allowing genuinely independent cache keys to fetch from Foreman in parallel. Only HTTP 200 responses are cached — errors bypass the cache entirely. Cache keys include sorted query parameters and auth context (Authorization header, REMOTE_USER, or anonymous) to ensure correct isolation between different registration configurations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
d741ddb to
7447343
Compare
|
Added identity-safe mutex eviction: after the cache entry is written, the per-key mutex is removed from Without this, |
Proxied or cached? |
|
oh I meant cached, yeah |
|
I haven't seen it done at the proxy level yet. |
|
Friendly ping when you have a moment: I addressed the earlier feedback and the branch is ready for another look. I'd really appreciate a review when convenient. Thanks! |
|
Regarding your question and an "extended" answer: I know we can do it at the Apache level, but we haven't done it neither in smart-proxy nor in the main foreman server, so I'm hesitant to introduce a change in the architecture at that level. |
@ehelms I would like to get your thoughts on this (caching the global reg script, and where that caching should live.) |
|
...and when I find content for which we need to have authentication, that usually means that we'd need to replicate all the authentication at the Apache level, so in those cases I prefer to add the caching at the layer that manages the content. |
What happens if the registration contains other configuration parameters beyond those 4? |
|
Good catch, that sentence is imprecise. The cache key is not limited to those 4 fields; the implementation uses the full normalized query string, so any additional registration configuration passed as query params gets its own cache entry as well. On top of that, the cache is also partitioned by auth context, since the rendered script can differ there too. Those 4 params were just the motivating examples from the bulk-registration case, not the full key definition. I can update the PR description to make that clearer. |
|
I think this is a good performance improvement for those scenarios mentioned, and that the 5 minute TTL feels right. |
ehelms
left a comment
There was a problem hiding this comment.
Design wise from me, I gave the code a cursory review.
Problem
GET /registerreturns the same shell script for all hosts sharing the same registration parameters (org, location, hostgroup, activation keys). Under bulk registration — 100+ hosts hitting the same capsule simultaneously — this endpoint is called once per host, each time proxying to Foreman and waiting for the ERB template to render (~103ms in profiling). With WEBrick's MaxClients=100, all threads can be held waiting for identical Foreman responses.Solution
Cache the rendered script in memory (5-minute TTL) keyed on a canonical form of the request query string (parameters sorted alphabetically, so requests differing only in parameter order share the same cache entry).
Per-key double-checked locking prevents the thundering herd on a cold cache while allowing concurrent requests for genuinely different keys (e.g. different activation keys) to fetch from Foreman in parallel without blocking each other:
The per-key Mutex is evicted from KEY_MUTEXES immediately after the script is written to cache, so KEY_MUTEXES is empty under steady state and bounded to in-flight fetches only.
Only 2xx responses are cached — errors never poison the cache. SCRIPT_CACHE uses Concurrent::Map (already a smart-proxy dependency) for lock-free reads that are safe on all Ruby VMs.
Test plan
Fixes #39208