Skip to content

Commit d42f03a

Browse files
committed
Add tests for LRU cache behavior in RouteHandler, ensuring eviction and caching logic works as expected
1 parent cbf5831 commit d42f03a

File tree

2 files changed

+111
-1
lines changed

2 files changed

+111
-1
lines changed

spec/route_handler_spec.cr

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,114 @@ describe "Kemal::RouteHandler" do
179179
client_response.body.should eq("home page")
180180
client_response.headers.has_key?("Location").should eq(true)
181181
end
182+
183+
context "LRU cache" do
184+
it "evicts least recently used entries instead of clearing entirely" do
185+
# Use a small capacity to make the test fast and deterministic
186+
small_capacity = 8
187+
# Replace the cache instance with a smaller-capacity LRU for this test
188+
Kemal::RouteHandler::INSTANCE.cached_routes = Kemal::LRUCache(String, Radix::Result(Kemal::Route)).new(small_capacity)
189+
190+
# Define more routes than capacity
191+
0.upto(15) do |i|
192+
get "/lru_eviction_#{i}" do
193+
"ok"
194+
end
195+
end
196+
197+
# Access the first `small_capacity` routes to fill the cache
198+
0.upto(small_capacity - 1) do |i|
199+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_eviction_#{i}")
200+
end
201+
202+
Kemal::RouteHandler::INSTANCE.cached_routes.size.should eq small_capacity
203+
204+
# Access some new routes to trigger eviction
205+
small_capacity.upto(small_capacity + 3) do |i|
206+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_eviction_#{i}")
207+
end
208+
209+
# Cache should still be capped at capacity
210+
Kemal::RouteHandler::INSTANCE.cached_routes.size.should eq small_capacity
211+
end
212+
213+
it "retains recently used keys and evicts the least recently used" do
214+
small_capacity = 4
215+
Kemal::RouteHandler::INSTANCE.cached_routes = Kemal::LRUCache(String, Radix::Result(Kemal::Route)).new(small_capacity)
216+
217+
0.upto(5) do |i|
218+
get "/lru_recency_#{i}" do
219+
"ok"
220+
end
221+
end
222+
223+
# Fill cache with 0..3
224+
0.upto(3) do |i|
225+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_recency_#{i}")
226+
end
227+
228+
# Touch 0 and 1 to make them most recent
229+
[0, 1].each do |i|
230+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_recency_#{i}")
231+
end
232+
233+
# Insert 4 -> should evict least recent among {2,3}
234+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_recency_4")
235+
236+
# Insert 5 -> should evict the other of {2,3}
237+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_recency_5")
238+
239+
# Now 0 and 1 must still resolve from cache, and size is capped
240+
Kemal::RouteHandler::INSTANCE.cached_routes.size.should eq small_capacity
241+
242+
# A fresh lookup for 0 and 1 should be cache hits and not raise
243+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_recency_0").found?.should be_true
244+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_recency_1").found?.should be_true
245+
end
246+
247+
it "caches HEAD fallback GET lookups without growing beyond 1 for same path" do
248+
cap = 16
249+
Kemal::RouteHandler::INSTANCE.cached_routes = Kemal::LRUCache(String, Radix::Result(Kemal::Route)).new(cap)
250+
251+
get "/head_fallback" do
252+
"ok"
253+
end
254+
255+
# First HEAD should fallback to GET and cache one entry keyed by HEAD+path
256+
Kemal::RouteHandler::INSTANCE.lookup_route("HEAD", "/head_fallback").found?.should be_true
257+
Kemal::RouteHandler::INSTANCE.cached_routes.size.should eq 1
258+
259+
# Second HEAD lookup should be a cache hit; size must remain 1
260+
Kemal::RouteHandler::INSTANCE.lookup_route("HEAD", "/head_fallback").found?.should be_true
261+
Kemal::RouteHandler::INSTANCE.cached_routes.size.should eq 1
262+
end
263+
264+
it "keeps size capped under heavy churn with large capacity" do
265+
large_capacity = 4096
266+
Kemal::RouteHandler::INSTANCE.cached_routes = Kemal::LRUCache(String, Radix::Result(Kemal::Route)).new(large_capacity)
267+
268+
0.upto(12000) do |i|
269+
get "/lru_heavy_#{i}" do
270+
"ok"
271+
end
272+
end
273+
274+
# Fill and churn beyond capacity
275+
0.upto(11999) do |i|
276+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_heavy_#{i}")
277+
end
278+
279+
Kemal::RouteHandler::INSTANCE.cached_routes.size.should eq large_capacity
280+
281+
# Additional churn should not increase size
282+
12000.upto(14000) do |i|
283+
get "/lru_heavy_more_#{i}" do
284+
"ok"
285+
end
286+
Kemal::RouteHandler::INSTANCE.lookup_route("GET", "/lru_heavy_more_#{i}")
287+
end
288+
289+
Kemal::RouteHandler::INSTANCE.cached_routes.size.should eq large_capacity
290+
end
291+
end
182292
end

spec/spec_helper.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,6 @@ Spec.after_each do
9393
Kemal.config.clear
9494
Kemal::FilterHandler::INSTANCE.tree = Radix::Tree(Array(Kemal::FilterHandler::FilterBlock)).new
9595
Kemal::RouteHandler::INSTANCE.routes = Radix::Tree(Route).new
96-
Kemal::RouteHandler::INSTANCE.cached_routes = Hash(String, Radix::Result(Route)).new
96+
Kemal::RouteHandler::INSTANCE.cached_routes = Kemal::LRUCache(String, Radix::Result(Kemal::Route)).new(1024)
9797
Kemal::WebSocketHandler::INSTANCE.routes = Radix::Tree(WebSocket).new
9898
end

0 commit comments

Comments
 (0)