@@ -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
182292end
0 commit comments