11require " radix"
22
33module Kemal
4+ # Small, private LRU cache used by the router to avoid full cache clears
5+ # when many distinct paths are accessed. Keeps get/put at O(1).
6+ # This is intentionally minimal and file-local to avoid API surface.
7+ class LRUCache (K , V )
8+ # Doubly-linked list node
9+ class Node (K , V )
10+ property key : K
11+ property value : V
12+ property prev : Node (K , V )?
13+ property next : Node (K , V )?
14+
15+ def initialize (@key : K , @value : V )
16+ @prev = nil
17+ @next = nil
18+ end
19+ end
20+
21+ @capacity : Int32
22+ @map : Hash (K , Node (K , V ))
23+ @head : Node (K , V )? # most-recent
24+ @tail : Node (K , V )? # least-recent
25+
26+ def initialize (@capacity : Int32 )
27+ @map = Hash (K , Node (K , V )).new
28+ @head = nil
29+ @tail = nil
30+ end
31+
32+ def size : Int32
33+ @map .size
34+ end
35+
36+ def get (key : K ) : V ?
37+ if node = @map [key]?
38+ move_to_front(node)
39+ return node.value
40+ end
41+ nil
42+ end
43+
44+ def put (key : K , value : V ) : Nil
45+ if node = @map [key]?
46+ node.value = value
47+ move_to_front(node)
48+ return
49+ end
50+
51+ node = Node (K , V ).new(key, value)
52+ @map [key] = node
53+ insert_front(node)
54+ evict_if_needed
55+ end
56+
57+ private def insert_front (node : Node (K , V ))
58+ node.prev = nil
59+ node.next = @head
60+ @head .try { |h | h.prev = node }
61+ @head = node
62+ @tail = node if @tail .nil?
63+ end
64+
65+ private def move_to_front (node : Node (K , V ))
66+ return if node == @head
67+
68+ # unlink
69+ prev = node.prev
70+ nxt = node.next
71+ prev.try { |p | p.next = nxt }
72+ nxt.try { |n | n.prev = prev }
73+
74+ # fix tail if needed
75+ if node == @tail
76+ @tail = prev
77+ end
78+
79+ # insert at head
80+ node.prev = nil
81+ node.next = @head
82+ @head .try { |h | h.prev = node }
83+ @head = node
84+ end
85+
86+ private def evict_if_needed
87+ return if @map .size <= @capacity
88+
89+ if lru = @tail
90+ # unlink tail
91+ prev = lru.prev
92+ if prev
93+ prev.next = nil
94+ @tail = prev
95+ else
96+ # only one element
97+ @head = nil
98+ @tail = nil
99+ end
100+ @map .delete(lru.key)
101+ end
102+ end
103+ end
104+
4105 class RouteHandler
5106 include HTTP ::Handler
6107
@@ -10,7 +111,7 @@ module Kemal
10111
11112 def initialize
12113 @routes = Radix ::Tree (Route ).new
13- @cached_routes = Hash (String , Radix ::Result (Route )).new
114+ @cached_routes = LRUCache (String , Radix ::Result (Route )).new( CACHED_ROUTES_LIMIT )
14115 end
15116
16117 def call (context : HTTP ::Server ::Context )
@@ -26,7 +127,7 @@ module Kemal
26127 def lookup_route (verb : String , path : String )
27128 lookup_path = radix_path(verb, path)
28129
29- if cached_route = @cached_routes [ lookup_path]?
130+ if cached_route = @cached_routes .get( lookup_path)
30131 return cached_route
31132 end
32133
@@ -38,8 +139,7 @@ module Kemal
38139 end
39140
40141 if route.found?
41- @cached_routes .clear if @cached_routes .size == CACHED_ROUTES_LIMIT
42- @cached_routes [lookup_path] = route
142+ @cached_routes .put(lookup_path, route)
43143 end
44144
45145 route
0 commit comments