Skip to content

Commit cbf5831

Browse files
committed
Add LRUCache implementation to optimize route caching
1 parent c673b39 commit cbf5831

File tree

1 file changed

+104
-4
lines changed

1 file changed

+104
-4
lines changed

src/kemal/route_handler.cr

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,107 @@
11
require "radix"
22

33
module 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

Comments
 (0)