Class: Middleware::AnonymousCache::Helper
- Inherits:
-
Object
- Object
- Middleware::AnonymousCache::Helper
- Defined in:
- lib/middleware/anonymous_cache.rb
Overview
This gives us an API to insert anonymous cache segments
Constant Summary collapse
- RACK_SESSION =
"rack.session"
- USER_AGENT =
"HTTP_USER_AGENT"
- ACCEPT_ENCODING =
"HTTP_ACCEPT_ENCODING"
- DISCOURSE_RENDER =
"HTTP_DISCOURSE_RENDER"
- REDIS_STORE_SCRIPT =
DiscourseRedis::EvalHelper.new <<~LUA local current = redis.call("incr", KEYS[1]) redis.call("expire",KEYS[1],ARGV[1]) return current LUA
- MIN_TIME_TO_CHECK =
0.05
- ADP =
"action_dispatch.request.parameters"
Instance Method Summary collapse
- #blocked_crawler? ⇒ Boolean
-
#cache(result, env = {}) ⇒ Object
NOTE in an ideal world cache still serves out cached content except for one magic worker that fills it up, this avoids a herd killing you, we can probably do this using a job or redis tricks but coordinating this is tricky.
- #cache_duration ⇒ Object
- #cache_key ⇒ Object
- #cache_key_body ⇒ Object
- #cache_key_count ⇒ Object
- #cache_key_other ⇒ Object
- #cacheable? ⇒ Boolean
- #cached(env = {}) ⇒ Object
- #check_logged_in_rate_limit! ⇒ Object
- #clear_cache ⇒ Object
- #compress(val) ⇒ Object
- #decompress(val) ⇒ Object
- #force_anonymous! ⇒ Object
- #get? ⇒ Boolean
- #has_auth_cookie? ⇒ Boolean
-
#initialize(env, request = nil) ⇒ Helper
constructor
A new instance of Helper.
- #is_crawler? ⇒ Boolean (also: #key_is_crawler?)
- #is_mobile=(val) ⇒ Object
- #is_mobile? ⇒ Boolean (also: #key_is_mobile?)
- #key_cache_theme_ids ⇒ Object
- #key_compress_anon ⇒ Object
- #key_has_brotli? ⇒ Boolean
- #key_is_modern_mobile_device? ⇒ Boolean
- #key_is_old_browser? ⇒ Boolean
- #key_locale ⇒ Object
- #logged_in_anon_limiter ⇒ Object
- #no_cache_bypass ⇒ Object
- #should_force_anonymous? ⇒ Boolean
- #theme_ids ⇒ Object
Constructor Details
#initialize(env, request = nil) ⇒ Helper
Returns a new instance of Helper.
74 75 76 77 |
# File 'lib/middleware/anonymous_cache.rb', line 74 def initialize(env, request = nil) @env = env @request = request || Rack::Request.new(@env) end |
Instance Method Details
#blocked_crawler? ⇒ Boolean
79 80 81 82 83 84 85 86 |
# File 'lib/middleware/anonymous_cache.rb', line 79 def blocked_crawler? @request.get? && !@request.xhr? && !@request.path.ends_with?("robots.txt") && !@request.path.ends_with?("srv/status") && @request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? && @env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY].nil? && CrawlerDetection.is_blocked_crawler?(@env[USER_AGENT]) end |
#cache(result, env = {}) ⇒ Object
NOTE in an ideal world cache still serves out cached content except for one magic worker
that fills it up, this avoids a herd killing you, we can probably do this using a job or redis tricks
but coordinating this is tricky
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 |
# File 'lib/middleware/anonymous_cache.rb', line 293 def cache(result, env = {}) return result if GlobalSetting.anon_cache_store_threshold == 0 status, headers, response = result if status == 200 && cache_duration if GlobalSetting.anon_cache_store_threshold > 1 count = REDIS_STORE_SCRIPT.eval(Discourse.redis, [cache_key_count], [cache_duration]) # technically lua will cast for us, but might as well be # prudent here, hence the to_i if count.to_i < GlobalSetting.anon_cache_store_threshold headers["X-Discourse-Cached"] = "skip" return status, headers, response end end headers_stripped = headers.dup.delete_if { |k, _| %w[Set-Cookie X-MiniProfiler-Ids].include? k } headers_stripped["X-Discourse-Cached"] = "true" parts = [] response.each { |part| parts << part } if req_params = env[ADP] headers_stripped[ADP] = { "action" => req_params["action"], "controller" => req_params["controller"], } end Discourse.redis.setex(cache_key_body, cache_duration, compress(parts.join)) Discourse.redis.setex(cache_key_other, cache_duration, [status, headers_stripped].to_json) headers["X-Discourse-Cached"] = "store" else parts = response end [status, headers, parts] end |
#cache_duration ⇒ Object
286 287 288 |
# File 'lib/middleware/anonymous_cache.rb', line 286 def cache_duration @env["ANON_CACHE_DURATION"] end |
#cache_key ⇒ Object
152 153 154 155 156 157 158 159 |
# File 'lib/middleware/anonymous_cache.rb', line 152 def cache_key return @cache_key if defined?(@cache_key) @cache_key = +"ANON_CACHE_#{@env["HTTP_ACCEPT"]}_#{@env[Rack::RACK_URL_SCHEME]}_#{@env["HTTP_HOST"]}#{@env["REQUEST_URI"]}" @cache_key << AnonymousCache.build_cache_key(self) @cache_key end |
#cache_key_body ⇒ Object
183 184 185 |
# File 'lib/middleware/anonymous_cache.rb', line 183 def cache_key_body @cache_key_body ||= "#{cache_key}_body" end |
#cache_key_count ⇒ Object
179 180 181 |
# File 'lib/middleware/anonymous_cache.rb', line 179 def cache_key_count @cache_key_count ||= "#{cache_key}_count" end |
#cache_key_other ⇒ Object
187 188 189 |
# File 'lib/middleware/anonymous_cache.rb', line 187 def cache_key_other @cache_key_other || "#{cache_key}_other" end |
#cacheable? ⇒ Boolean
249 250 251 252 253 254 |
# File 'lib/middleware/anonymous_cache.rb', line 249 def cacheable? !!( GlobalSetting.anon_cache_store_threshold > 0 && ! && get? && no_cache_bypass ) end |
#cached(env = {}) ⇒ Object
274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/middleware/anonymous_cache.rb', line 274 def cached(env = {}) if body = decompress(Discourse.redis.get(cache_key_body)) if other = Discourse.redis.get(cache_key_other) other = JSON.parse(other) if req_params = other[1].delete(ADP) env[ADP] = req_params end [other[0], other[1], [body]] end end end |
#check_logged_in_rate_limit! ⇒ Object
230 231 232 |
# File 'lib/middleware/anonymous_cache.rb', line 230 def check_logged_in_rate_limit! !logged_in_anon_limiter.performed!(raise_error: false) end |
#clear_cache ⇒ Object
334 335 336 337 |
# File 'lib/middleware/anonymous_cache.rb', line 334 def clear_cache Discourse.redis.del(cache_key_body) Discourse.redis.del(cache_key_other) end |
#compress(val) ⇒ Object
256 257 258 259 260 261 262 263 |
# File 'lib/middleware/anonymous_cache.rb', line 256 def compress(val) if val && GlobalSetting.compress_anon_cache require "lz4-ruby" if !defined?(LZ4) LZ4.compress(val) else val end end |
#decompress(val) ⇒ Object
265 266 267 268 269 270 271 272 |
# File 'lib/middleware/anonymous_cache.rb', line 265 def decompress(val) if val && GlobalSetting.compress_anon_cache require "lz4-ruby" if !defined?(LZ4) LZ4.uncompress(val) else val end end |
#force_anonymous! ⇒ Object
207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/middleware/anonymous_cache.rb', line 207 def force_anonymous! @env[Auth::DefaultCurrentUserProvider::USER_API_KEY] = nil @env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY] = nil @env["HTTP_COOKIE"] = nil @env["HTTP_DISCOURSE_LOGGED_IN"] = nil @env["rack.request.cookie.hash"] = {} @env["rack.request.cookie.string"] = "" @env["_bypass_cache"] = nil request = Rack::Request.new(@env) request.delete_param("api_username") request.delete_param("api_key") end |
#get? ⇒ Boolean
191 192 193 |
# File 'lib/middleware/anonymous_cache.rb', line 191 def get? @env["REQUEST_METHOD"] == "GET" end |
#has_auth_cookie? ⇒ Boolean
195 196 197 |
# File 'lib/middleware/anonymous_cache.rb', line 195 def CurrentUser.(@env) end |
#is_crawler? ⇒ Boolean Also known as: key_is_crawler?
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/middleware/anonymous_cache.rb', line 123 def is_crawler? @is_crawler ||= begin user_agent = @env[USER_AGENT] if @env[DISCOURSE_RENDER] == "crawler" || CrawlerDetection.crawler?(user_agent, @env["HTTP_VIA"]) :true else if user_agent.downcase.include?("discourse") && !user_agent.downcase.include?("mobile") :true else :false end end end @is_crawler == :true end |
#is_mobile=(val) ⇒ Object
88 89 90 |
# File 'lib/middleware/anonymous_cache.rb', line 88 def is_mobile=(val) @is_mobile = val ? :true : :false end |
#is_mobile? ⇒ Boolean Also known as: key_is_mobile?
92 93 94 95 96 97 98 99 100 101 102 103 104 |
# File 'lib/middleware/anonymous_cache.rb', line 92 def is_mobile? @is_mobile ||= begin session = @env[RACK_SESSION] # don't initialize params until later # otherwise you get a broken params on the request params = {} MobileDetection.resolve_mobile_view!(@env[USER_AGENT], params, session) ? :true : :false end @is_mobile == :true end |
#key_cache_theme_ids ⇒ Object
161 162 163 |
# File 'lib/middleware/anonymous_cache.rb', line 161 def key_cache_theme_ids theme_ids.join(",") end |
#key_compress_anon ⇒ Object
165 166 167 |
# File 'lib/middleware/anonymous_cache.rb', line 165 def key_compress_anon GlobalSetting.compress_anon_cache end |
#key_has_brotli? ⇒ Boolean
107 108 109 110 111 112 113 |
# File 'lib/middleware/anonymous_cache.rb', line 107 def key_has_brotli? @has_brotli ||= begin @env[ACCEPT_ENCODING].to_s =~ /br/ ? :true : :false end @has_brotli == :true end |
#key_is_modern_mobile_device? ⇒ Boolean
144 145 146 |
# File 'lib/middleware/anonymous_cache.rb', line 144 def key_is_modern_mobile_device? MobileDetection.modern_mobile_device?(@env[USER_AGENT]) if @env[USER_AGENT] end |
#key_is_old_browser? ⇒ Boolean
148 149 150 |
# File 'lib/middleware/anonymous_cache.rb', line 148 def key_is_old_browser? CrawlerDetection.show_browser_update?(@env[USER_AGENT]) if @env[USER_AGENT] end |
#key_locale ⇒ Object
115 116 117 118 119 120 121 |
# File 'lib/middleware/anonymous_cache.rb', line 115 def key_locale if locale = Discourse.anonymous_locale(@request) locale else "" # No need to key, it is the same for all anon users end end |
#logged_in_anon_limiter ⇒ Object
220 221 222 223 224 225 226 227 228 |
# File 'lib/middleware/anonymous_cache.rb', line 220 def logged_in_anon_limiter @logged_in_anon_limiter ||= RateLimiter.new( nil, "logged_in_anon_cache_#{@env["HTTP_HOST"]}/#{@env["REQUEST_URI"]}", GlobalSetting.force_anonymous_min_per_10_seconds, 10, ) end |
#no_cache_bypass ⇒ Object
199 200 201 202 203 204 205 |
# File 'lib/middleware/anonymous_cache.rb', line 199 def no_cache_bypass request = Rack::Request.new(@env) request.["_bypass_cache"].nil? && (request.path != "/srv/status") && request[Auth::DefaultCurrentUserProvider::API_KEY].nil? && @env[Auth::DefaultCurrentUserProvider::HEADER_API_KEY].nil? && @env[Auth::DefaultCurrentUserProvider::USER_API_KEY].nil? end |
#should_force_anonymous? ⇒ Boolean
237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/middleware/anonymous_cache.rb', line 237 def should_force_anonymous? if (queue_time = @env["REQUEST_QUEUE_SECONDS"]) && get? if queue_time > GlobalSetting.force_anonymous_min_queue_seconds return check_logged_in_rate_limit! elsif queue_time >= MIN_TIME_TO_CHECK return check_logged_in_rate_limit! if !logged_in_anon_limiter.can_perform? end end false end |
#theme_ids ⇒ Object
169 170 171 172 173 174 175 176 177 |
# File 'lib/middleware/anonymous_cache.rb', line 169 def theme_ids ids, _ = @request.["theme_ids"]&.split("|") id = ids&.split(",")&.map(&:to_i)&.first if id && Guardian.new.allow_themes?([id]) Theme.transform_ids(id) else [] end end |