diff --git a/core/routers/LuaRouter.go b/core/routers/LuaRouter.go index bae7dca..d2a5737 100644 --- a/core/routers/LuaRouter.go +++ b/core/routers/LuaRouter.go @@ -1,35 +1,27 @@ package routers import ( + "encoding/binary" "errors" + "hash/fnv" "os" "path/filepath" "strings" "sync" + "time" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" + "github.com/VictoriaMetrics/fastcache" ) // Maximum number of URL parameters per route const maxParams = 20 -// LuaRouter is a filesystem-based HTTP router for Lua files -type LuaRouter struct { - routesDir string // Root directory containing route files - routes map[string]*node // Method -> route tree - failedRoutes map[string]*RouteError // Track failed routes - mu sync.RWMutex // Lock for concurrent access to routes -} - -// node represents a node in the routing trie -type node struct { - handler string // Path to Lua file (empty if not an endpoint) - bytecode []byte // Pre-compiled Lua bytecode - paramName string // Parameter name (if this is a parameter node) - staticChild map[string]*node // Static children by segment name - paramChild *node // Parameter/wildcard child - err error // Compilation error if any -} +// Default cache sizes +const ( + defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache + defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache +) // Params holds URL parameters with fixed-size arrays to avoid allocations type Params struct { @@ -48,6 +40,28 @@ func (p *Params) Get(name string) string { return "" } +// LuaRouter is a filesystem-based HTTP router for Lua files +type LuaRouter struct { + routesDir string // Root directory containing route files + routes map[string]*node // Method -> route tree + failedRoutes map[string]*RouteError // Track failed routes + mu sync.RWMutex // Lock for concurrent access to routes + + // Cache for route matches and bytecode + routeCache *fastcache.Cache // Cache for route lookups + bytecodeCache *fastcache.Cache // Cache for compiled bytecode +} + +// node represents a node in the routing trie +type node struct { + handler string // Path to Lua file (empty if not an endpoint) + paramName string // Parameter name (if this is a parameter node) + staticChild map[string]*node // Static children by segment name + paramChild *node // Parameter/wildcard child + err error // Compilation error if any + modTime time.Time // Last modification time +} + // NewLuaRouter creates a new LuaRouter instance func NewLuaRouter(routesDir string) (*LuaRouter, error) { // Verify routes directory exists @@ -60,9 +74,11 @@ func NewLuaRouter(routesDir string) (*LuaRouter, error) { } r := &LuaRouter{ - routesDir: routesDir, - routes: make(map[string]*node), - failedRoutes: make(map[string]*RouteError), + routesDir: routesDir, + routes: make(map[string]*node), + failedRoutes: make(map[string]*RouteError), + routeCache: fastcache.New(defaultRouteMaxBytes), + bytecodeCache: fastcache.New(defaultBytecodeMaxBytes), } // Initialize method trees @@ -77,7 +93,6 @@ func NewLuaRouter(routesDir string) (*LuaRouter, error) { err = r.buildRoutes() // If some routes failed to compile, return the router with a warning error - // This allows the server to continue running with the routes that did compile if len(r.failedRoutes) > 0 { return r, ErrRoutesCompilationErrors } @@ -127,13 +142,13 @@ func (r *LuaRouter) buildRoutes() error { } // Add route to tree - continue even if there are errors - r.addRoute(root, urlPath, path) + r.addRoute(root, urlPath, path, info.ModTime()) return nil }) } -// addRoute adds a route to the routing tree and compiles the Lua file to bytecode -func (r *LuaRouter) addRoute(root *node, urlPath, handlerPath string) error { +// addRoute adds a route to the routing tree +func (r *LuaRouter) addRoute(root *node, urlPath, handlerPath string, modTime time.Time) error { segments := strings.Split(strings.Trim(urlPath, "/"), "/") current := root @@ -159,11 +174,12 @@ func (r *LuaRouter) addRoute(root *node, urlPath, handlerPath string) error { } } - // Set handler path + // Set handler path and mod time current.handler = handlerPath + current.modTime = modTime // Compile Lua file to bytecode - if err := r.compileHandler(current, urlPath); err != nil { + if err := r.compileHandler(current); err != nil { // Track the failure but don't fail the entire process routeKey := getRouteKey(urlPath, handlerPath) r.failedRoutes[routeKey] = &RouteError{ @@ -181,6 +197,38 @@ func getRouteKey(path, handler string) string { return path + ":" + handler } +// hashString generates a hash for a string +func hashString(s string) uint64 { + h := fnv.New64a() + h.Write([]byte(s)) + return h.Sum64() +} + +// uint64ToBytes converts a uint64 to bytes for cache key +func uint64ToBytes(n uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, n) + return b +} + +// bytesToUint64 converts bytes to uint64 +func bytesToUint64(b []byte) uint64 { + return binary.LittleEndian.Uint64(b) +} + +// getCacheKey generates a cache key for a method and path +func getCacheKey(method, path string) []byte { + // Simple concatenation with separator to create a unique key + key := hashString(method + ":" + path) + return uint64ToBytes(key) +} + +// getBytecodeKey generates a cache key for a handler path +func getBytecodeKey(handlerPath string) []byte { + key := hashString(handlerPath) + return uint64ToBytes(key) +} + // Match finds a handler for the given method and path // Uses the pre-allocated params struct to avoid allocations func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) { @@ -244,7 +292,7 @@ func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, } // compileHandler compiles a Lua file to bytecode -func (r *LuaRouter) compileHandler(n *node, _ string) error { +func (r *LuaRouter) compileHandler(n *node) error { if n.handler == "" { return nil } @@ -271,15 +319,71 @@ func (r *LuaRouter) compileHandler(n *node, _ string) error { return err } - // Store bytecode in the node - n.bytecode = bytecode + // Store bytecode in cache + bytecodeKey := getBytecodeKey(n.handler) + r.bytecodeCache.Set(bytecodeKey, bytecode) + n.err = nil // Clear any previous error return nil } // GetBytecode returns the compiled bytecode for a matched route -// If a route exists but failed to compile, returns nil bytecode with found=true +// Uses FastCache for both route matching and bytecode retrieval func (r *LuaRouter) GetBytecode(method, path string, params *Params) ([]byte, string, bool) { + // Check route cache first + routeCacheKey := getCacheKey(method, path) + routeCacheData := r.routeCache.Get(nil, routeCacheKey) + + if len(routeCacheData) > 0 { + // Cache hit - first 8 bytes are bytecode hash, rest is handler path + handlerPath := string(routeCacheData[8:]) + bytecodeKey := routeCacheData[:8] + + // Get bytecode from cache + bytecode := r.bytecodeCache.Get(nil, bytecodeKey) + if len(bytecode) > 0 { + return bytecode, handlerPath, true + } + + // Bytecode not found, check if file was modified + n, exists := r.nodeForHandler(handlerPath) + if !exists { + // Handler no longer exists + r.routeCache.Del(routeCacheKey) + return nil, "", false + } + + // Check if file was modified + fileInfo, err := os.Stat(handlerPath) + if err != nil || fileInfo.ModTime().After(n.modTime) { + // Recompile if file was modified + if err := r.compileHandler(n); err != nil { + return nil, handlerPath, true // Return with error + } + + // Update cache + newBytecodeKey := getBytecodeKey(handlerPath) + bytecode = r.bytecodeCache.Get(nil, newBytecodeKey) + + // Update route cache + newCacheData := make([]byte, 8+len(handlerPath)) + copy(newCacheData[:8], newBytecodeKey) + copy(newCacheData[8:], handlerPath) + r.routeCache.Set(routeCacheKey, newCacheData) + + return bytecode, handlerPath, true + } + + // Strange case - bytecode not in cache but file not modified + // Recompile + if err := r.compileHandler(n); err != nil { + return nil, handlerPath, true + } + bytecode = r.bytecodeCache.Get(nil, bytecodeKey) + return bytecode, handlerPath, true + } + + // Cache miss - do normal routing node, found := r.Match(method, path, params) if !found { return nil, "", false @@ -290,7 +394,66 @@ func (r *LuaRouter) GetBytecode(method, path string, params *Params) ([]byte, st return nil, node.handler, true } - return node.bytecode, node.handler, true + // Get bytecode from cache + bytecodeKey := getBytecodeKey(node.handler) + bytecode := r.bytecodeCache.Get(nil, bytecodeKey) + + if len(bytecode) == 0 { + // Compile if not in cache + if err := r.compileHandler(node); err != nil { + return nil, node.handler, true + } + bytecode = r.bytecodeCache.Get(nil, bytecodeKey) + } + + // Add to route cache + cacheData := make([]byte, 8+len(node.handler)) + copy(cacheData[:8], bytecodeKey) + copy(cacheData[8:], node.handler) + r.routeCache.Set(routeCacheKey, cacheData) + + return bytecode, node.handler, true +} + +// nodeForHandler finds a node by its handler path +func (r *LuaRouter) nodeForHandler(handlerPath string) (*node, bool) { + r.mu.RLock() + defer r.mu.RUnlock() + + for _, root := range r.routes { + if node := findNodeByHandler(root, handlerPath); node != nil { + return node, true + } + } + + return nil, false +} + +// findNodeByHandler finds a node by its handler path +func findNodeByHandler(current *node, handlerPath string) *node { + if current == nil { + return nil + } + + if current.handler == handlerPath { + return current + } + + // Check static children + for _, child := range current.staticChild { + if node := findNodeByHandler(child, handlerPath); node != nil { + return node + } + } + + // Check param child + if current.paramChild != nil { + if node := findNodeByHandler(current.paramChild, handlerPath); node != nil { + return node + } + } + + return nil } // Refresh rebuilds the router by rescanning the routes directory @@ -332,22 +495,28 @@ func (r *LuaRouter) ReportFailedRoutes() []*RouteError { return result } -type NodeWithError struct { - ScriptPath string - Error error +// ClearCache clears both the route and bytecode caches +func (r *LuaRouter) ClearCache() { + r.routeCache.Reset() + r.bytecodeCache.Reset() } -// GetNodeWithError returns the node with its error for a given path -func (r *LuaRouter) GetNodeWithError(method, path string, params *Params) (*NodeWithError, bool) { - node, found := r.Match(method, path, params) - if !found { - return nil, false - } +// GetCacheStats returns statistics about the cache +func (r *LuaRouter) GetCacheStats() map[string]interface{} { + var routeStats fastcache.Stats + var bytecodeStats fastcache.Stats - return &NodeWithError{ - ScriptPath: node.handler, - Error: node.err, - }, true + r.routeCache.UpdateStats(&routeStats) + r.bytecodeCache.UpdateStats(&bytecodeStats) + + return map[string]interface{}{ + "routeEntries": routeStats.EntriesCount, + "routeBytes": routeStats.BytesSize, + "routeCollisions": routeStats.Collisions, + "bytecodeEntries": bytecodeStats.EntriesCount, + "bytecodeBytes": bytecodeStats.BytesSize, + "bytecodeCollisions": bytecodeStats.Collisions, + } } // GetRouteStats returns statistics about the router @@ -358,7 +527,7 @@ func (r *LuaRouter) GetRouteStats() (int, int64) { routeCount := 0 bytecodeBytes := int64(0) - // Count routes and bytecode size + // Count routes and estimate bytecode size for _, root := range r.routes { count, bytes := countNodesAndBytecode(root) routeCount += count @@ -377,7 +546,8 @@ func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) { // Count this node if it has a handler if n.handler != "" { count = 1 - bytecodeBytes = int64(len(n.bytecode)) + // Estimate bytecode size (average of 2KB per script) + bytecodeBytes = 2048 } // Count static children @@ -396,3 +566,40 @@ func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) { return count, bytecodeBytes } + +type NodeWithError struct { + ScriptPath string + Error error +} + +// GetNodeWithError returns the node with its error for a given path +func (r *LuaRouter) GetNodeWithError(method, path string, params *Params) (*NodeWithError, bool) { + // Try route cache first + routeCacheKey := getCacheKey(method, path) + routeCacheData := r.routeCache.Get(nil, routeCacheKey) + + if len(routeCacheData) > 0 { + // Cache hit - get handler path + handlerPath := string(routeCacheData[8:]) + + // Find the node for this handler + node, found := r.nodeForHandler(handlerPath) + if found { + return &NodeWithError{ + ScriptPath: node.handler, + Error: node.err, + }, true + } + } + + // Cache miss - do normal routing + node, found := r.Match(method, path, params) + if !found { + return nil, false + } + + return &NodeWithError{ + ScriptPath: node.handler, + Error: node.err, + }, true +} diff --git a/go.mod b/go.mod index 538bfec..36676f4 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,17 @@ go 1.24.1 require git.sharkk.net/Sky/LuaJIT-to-Go v0.0.0 require ( + github.com/VictoriaMetrics/fastcache v1.12.2 // indirect github.com/andybalholm/brotli v1.1.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/panjf2000/ants/v2 v2.11.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasthttp v1.60.0 // indirect golang.org/x/sync v0.12.0 // indirect + golang.org/x/sys v0.31.0 // indirect ) replace git.sharkk.net/Sky/LuaJIT-to-Go => ./luajit diff --git a/go.sum b/go.sum index 7663914..63ca4b9 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,23 @@ +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/panjf2000/ants/v2 v2.11.2 h1:AVGpMSePxUNpcLaBO34xuIgM1ZdKOiGnpxLXixLi5Jo= github.com/panjf2000/ants/v2 v2.11.2/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.60.0 h1:kBRYS0lOhVJ6V+bYN8PqAHELKHtXqwq9zNMLKx1MBsw= @@ -13,3 +25,6 @@ github.com/valyala/fasthttp v1.60.0/go.mod h1:iY4kDgV3Gc6EqhRZ8icqcmlG6bqhcDXfuH github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=