various optimizations, static file serving changes, break down luarouter

This commit is contained in:
Sky Johnson 2025-05-29 15:59:16 -05:00
parent 163e94d576
commit 5b698f31e4
16 changed files with 1145 additions and 1443 deletions

View File

@ -1,11 +1,12 @@
package http
import (
"bytes"
"context"
"sync"
"time"
"Moonshark/routers"
"Moonshark/router"
"Moonshark/runner"
"Moonshark/sessions"
"Moonshark/utils"
@ -17,9 +18,18 @@ import (
"github.com/valyala/fasthttp"
)
var (
//methodGET = []byte("GET")
methodPOST = []byte("POST")
methodPUT = []byte("PUT")
methodPATCH = []byte("PATCH")
debugPath = []byte("/debug/stats")
)
type Server struct {
luaRouter *routers.LuaRouter
staticRouter *routers.StaticRouter
luaRouter *router.LuaRouter
staticHandler fasthttp.RequestHandler
staticFS *fasthttp.FS
luaRunner *runner.Runner
fasthttpServer *fasthttp.Server
loggingEnabled bool
@ -28,29 +38,77 @@ type Server struct {
sessionManager *sessions.SessionManager
errorConfig utils.ErrorPageConfig
ctxPool sync.Pool
paramsPool sync.Pool
staticDir string
staticPrefix string
staticPrefixBytes []byte
// Cached error pages
cached404 []byte
cached500 []byte
errorCacheMu sync.RWMutex
}
func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter,
func New(luaRouter *router.LuaRouter, staticDir string,
runner *runner.Runner, loggingEnabled bool, debugMode bool,
overrideDir string, config *config.Config) *Server {
staticPrefix := config.Server.StaticPrefix
if staticPrefix == "" {
staticPrefix = "/static/"
}
if staticPrefix[0] != '/' {
staticPrefix = "/" + staticPrefix
}
if staticPrefix[len(staticPrefix)-1] != '/' {
staticPrefix = staticPrefix + "/"
}
s := &Server{
luaRouter: luaRouter,
staticRouter: staticRouter,
luaRunner: runner,
loggingEnabled: loggingEnabled,
debugMode: debugMode,
config: config,
sessionManager: sessions.GlobalSessionManager,
staticDir: staticDir,
staticPrefix: staticPrefix,
staticPrefixBytes: []byte(staticPrefix),
errorConfig: utils.ErrorPageConfig{
OverrideDir: overrideDir,
DebugMode: debugMode,
},
ctxPool: sync.Pool{
New: func() any {
return make(map[string]any, 8)
return make(map[string]any, 6)
},
},
paramsPool: sync.Pool{
New: func() any {
return make(map[string]any, 4)
},
},
}
// Pre-cache error pages
s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, ""))
s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error"))
// Setup static file serving
if staticDir != "" {
s.staticFS = &fasthttp.FS{
Root: staticDir,
IndexNames: []string{"index.html"},
GenerateIndexPages: false,
AcceptByteRange: true,
Compress: true,
CompressedFileSuffix: ".gz",
CompressBrotli: true,
CompressZstd: true,
PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1),
}
s.staticHandler = s.staticFS.NewRequestHandler()
}
s.fasthttpServer = &fasthttp.Server{
@ -58,11 +116,15 @@ func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter,
Name: "Moonshark/" + metadata.Version,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
MaxRequestBodySize: 16 << 20,
TCPKeepalive: true,
TCPKeepalivePeriod: 60 * time.Second,
ReduceMemoryUsage: true,
DisablePreParseMultipartForm: true,
DisableHeaderNamesNormalizing: true,
NoDefaultServerHeader: true,
StreamRequestBody: true,
}
return s
@ -79,56 +141,48 @@ func (s *Server) Shutdown(ctx context.Context) error {
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
start := time.Now()
method := string(ctx.Method())
path := string(ctx.Path())
methodBytes := ctx.Method()
pathBytes := ctx.Path()
if s.debugMode && path == "/debug/stats" {
// Fast path for debug stats
if s.debugMode && bytes.Equal(pathBytes, debugPath) {
s.handleDebugStats(ctx)
if s.loggingEnabled {
logger.LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
return
}
logger.Debug("Processing request %s %s", method, path)
// Fast path for static files
if s.staticHandler != nil && bytes.HasPrefix(pathBytes, s.staticPrefixBytes) {
s.staticHandler(ctx)
if s.loggingEnabled {
logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
return
}
params := &routers.Params{}
bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params)
// Lua route lookup - only allocate params if found
bytecode, scriptPath, routeErr, params, found := s.luaRouter.GetRouteInfo(methodBytes, pathBytes)
if found {
if len(bytecode) == 0 || routeErr != nil {
errorMsg := "Route exists but failed to compile. Check server logs for details."
if routeErr != nil {
errorMsg = routeErr.Error()
}
logger.Error("%s %s - %s", method, path, errorMsg)
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, errorMsg)))
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, routeErr)
} else {
logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count)
s.handleLuaRoute(ctx, bytecode, scriptPath, params, method, path)
}
} else if s.staticRouter != nil {
if _, found := s.staticRouter.Match(path); found {
s.staticRouter.ServeHTTP(ctx)
} else {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path)))
s.handleLuaRoute(ctx, bytecode, scriptPath, params, methodBytes, pathBytes)
}
} else {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound)
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path)))
s.send404(ctx, pathBytes)
}
if s.loggingEnabled {
logger.LogRequest(ctx.Response.StatusCode(), method, path, time.Since(start))
logger.LogRequest(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
}
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, params *routers.Params, method, path string) {
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string,
params *router.Params, methodBytes, pathBytes []byte) {
luaCtx := runner.NewHTTPContext(ctx)
defer luaCtx.Release()
@ -142,28 +196,47 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
session := s.sessionManager.GetSessionFromRequest(ctx)
sessionMap["id"] = session.ID
sessionMap["data"] = session.GetAll()
luaCtx.Set("method", method)
luaCtx.Set("path", path)
// Only get session data if not empty
if !session.IsEmpty() {
sessionMap["data"] = session.GetAll()
} else {
sessionMap["data"] = emptyMap
}
// Set basic context
luaCtx.Set("method", string(methodBytes))
luaCtx.Set("path", string(pathBytes))
luaCtx.Set("host", string(ctx.Host()))
luaCtx.Set("session", sessionMap)
if params.Count > 0 {
paramMap := make(map[string]any, params.Count)
// Handle params
if params != nil && params.Count > 0 {
paramMap := s.paramsPool.Get().(map[string]any)
for i := 0; i < params.Count; i++ {
paramMap[params.Keys[i]] = params.Values[i]
}
luaCtx.Set("params", paramMap)
defer func() {
for k := range paramMap {
delete(paramMap, k)
}
s.paramsPool.Put(paramMap)
}()
} else {
luaCtx.Set("params", emptyMap)
}
if method == "POST" || method == "PUT" || method == "PATCH" {
// Parse form data for POST/PUT/PATCH
if bytes.Equal(methodBytes, methodPOST) ||
bytes.Equal(methodBytes, methodPUT) ||
bytes.Equal(methodBytes, methodPATCH) {
if formData, err := ParseForm(ctx); err == nil {
luaCtx.Set("form", formData)
} else {
if s.debugMode {
logger.Warning("Error parsing form: %v", err)
}
luaCtx.Set("form", emptyMap)
}
} else {
@ -172,13 +245,13 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
if err != nil {
logger.Error("Error executing Lua route: %v", err)
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, err.Error())))
logger.Error("Lua execution error: %v", err)
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, err)
return
}
// Handle session updates
if len(response.SessionData) > 0 {
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
session.Clear()
delete(response.SessionData, "__clear_all")
@ -191,12 +264,40 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
session.Set(k, v)
}
}
}
s.sessionManager.ApplySessionCookie(ctx, session)
runner.ApplyResponse(response, ctx)
runner.ReleaseResponse(response)
}
func (s *Server) send404(ctx *fasthttp.RequestCtx, pathBytes []byte) {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound)
// Use cached 404 for common case
if len(pathBytes) == 1 && pathBytes[0] == '/' {
s.errorCacheMu.RLock()
ctx.SetBody(s.cached404)
s.errorCacheMu.RUnlock()
} else {
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, string(pathBytes))))
}
}
func (s *Server) sendError(ctx *fasthttp.RequestCtx, status int, pathBytes []byte, err error) {
ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(status)
if err == nil {
s.errorCacheMu.RLock()
ctx.SetBody(s.cached500)
s.errorCacheMu.RUnlock()
} else {
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, string(pathBytes), err.Error())))
}
}
func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
stats := utils.CollectSystemStats(s.config)
routeCount, bytecodeBytes := s.luaRouter.GetRouteStats()
@ -209,3 +310,24 @@ func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetBody([]byte(utils.DebugStatsPage(stats)))
}
// SetStaticCaching enables/disables static file caching
func (s *Server) SetStaticCaching(duration time.Duration) {
if s.staticFS != nil {
s.staticFS.CacheDuration = duration
s.staticHandler = s.staticFS.NewRequestHandler()
}
}
// GetStaticPrefix returns the URL prefix for static files
func (s *Server) GetStaticPrefix() string {
return s.staticPrefix
}
// UpdateErrorCache refreshes cached error pages
func (s *Server) UpdateErrorCache() {
s.errorCacheMu.Lock()
s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, ""))
s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error"))
s.errorCacheMu.Unlock()
}

41
main.go
View File

@ -13,7 +13,7 @@ import (
"time"
"Moonshark/http"
"Moonshark/routers"
"Moonshark/router"
"Moonshark/runner"
"Moonshark/sessions"
"Moonshark/utils/color"
@ -26,8 +26,7 @@ import (
// Moonshark represents the server and all its dependencies
type Moonshark struct {
Config *config.Config
LuaRouter *routers.LuaRouter
StaticRouter *routers.StaticRouter
LuaRouter *router.LuaRouter
LuaRunner *runner.Runner
HTTPServer *http.Server
cleanupFuncs []func() error
@ -143,7 +142,7 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) {
cfg.Server.Debug = true
}
if err := initRouters(moonshark); err != nil {
if err := initLuaRouter(moonshark); err != nil {
return nil, err
}
@ -155,9 +154,18 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) {
logger.Warning("Watcher setup failed: %v", err)
}
// Get static directory - empty string if it doesn't exist
staticDir := ""
if dirExists(cfg.Dirs.Static) {
staticDir = cfg.Dirs.Static
logger.Info("Static files enabled: %s", color.Apply(staticDir, color.Yellow))
} else {
logger.Warning("Static directory not found: %s", color.Apply(cfg.Dirs.Static, color.Yellow))
}
moonshark.HTTPServer = http.New(
moonshark.LuaRouter,
moonshark.StaticRouter,
staticDir,
moonshark.LuaRunner,
cfg.Server.HTTPLogging,
cfg.Server.Debug,
@ -165,6 +173,13 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) {
cfg,
)
// For development, disable caching. For production, enable it
if cfg.Server.Debug {
moonshark.HTTPServer.SetStaticCaching(0) // No caching in debug mode
} else {
moonshark.HTTPServer.SetStaticCaching(1 * time.Hour) // Cache for 1 hour in production
}
return moonshark, nil
}
@ -249,15 +264,15 @@ func dirExists(path string) bool {
return info.IsDir()
}
func initRouters(s *Moonshark) error {
func initLuaRouter(s *Moonshark) error {
if !dirExists(s.Config.Dirs.Routes) {
return fmt.Errorf("routes directory doesn't exist: %s", s.Config.Dirs.Routes)
}
var err error
s.LuaRouter, err = routers.NewLuaRouter(s.Config.Dirs.Routes)
s.LuaRouter, err = router.NewLuaRouter(s.Config.Dirs.Routes)
if err != nil {
if errors.Is(err, routers.ErrRoutesCompilationErrors) {
if errors.Is(err, router.ErrRoutesCompilationErrors) {
// Non-fatal, some routes failed
logger.Warning("Some routes failed to compile")
@ -272,16 +287,6 @@ func initRouters(s *Moonshark) error {
}
logger.Info("LuaRouter is g2g! %s", color.Set(s.Config.Dirs.Routes, color.Yellow))
if dirExists(s.Config.Dirs.Static) {
s.StaticRouter, err = routers.NewStaticRouter(s.Config.Dirs.Static)
if err != nil {
return fmt.Errorf("static router init failed: %v", err)
}
logger.Info("StaticRouter is g2g! %s", color.Apply(s.Config.Dirs.Static, color.Yellow))
} else {
logger.Warning("Static directory not found... %s", color.Apply(s.Config.Dirs.Static, color.Yellow))
}
return nil
}

90
router/build.go Normal file
View File

@ -0,0 +1,90 @@
package router
import (
"os"
"path/filepath"
"strings"
)
// buildRoutes scans the routes directory and builds the routing tree
func (r *LuaRouter) buildRoutes() error {
r.failedRoutes = make(map[string]*RouteError)
r.middlewareFiles = make(map[string][]string)
// First pass: collect all middleware files
err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err
}
if strings.TrimSuffix(info.Name(), ".lua") == "middleware" {
relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path))
if err != nil {
return err
}
fsPath := "/"
if relDir != "." {
fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/")
}
// Use filesystem path for middleware (includes groups)
r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path)
}
return nil
})
if err != nil {
return err
}
// Second pass: build routes with combined middleware + handler
return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err
}
fileName := strings.TrimSuffix(info.Name(), ".lua")
// Skip middleware files (already processed)
if fileName == "middleware" {
return nil
}
relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path))
if err != nil {
return err
}
fsPath := "/"
if relDir != "." {
fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/")
}
pathInfo := parsePathWithGroups(fsPath)
// Handle index.lua files
if fileName == "index" {
for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} {
root := r.routes[method]
node := r.findOrCreateNode(root, pathInfo.urlPath)
node.indexFile = path
node.modTime = info.ModTime()
node.fsPath = pathInfo.fsPath
r.compileWithMiddleware(node, pathInfo.fsPath, path)
}
return nil
}
// Handle method files
method := strings.ToUpper(fileName)
root, exists := r.routes[method]
if !exists {
return nil
}
r.addRoute(root, pathInfo, path, info.ModTime())
return nil
})
}

62
router/cache.go Normal file
View File

@ -0,0 +1,62 @@
package router
import (
"encoding/binary"
"hash/fnv"
"time"
"github.com/VictoriaMetrics/fastcache"
)
// 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
}
// getCacheKey generates a cache key for a method and path
func getCacheKey(method, path string) []byte {
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)
}
// ClearCache clears all caches
func (r *LuaRouter) ClearCache() {
r.routeCache.Reset()
r.bytecodeCache.Reset()
r.middlewareCache = make(map[string][]byte)
r.sourceCache = make(map[string][]byte)
r.sourceMtimes = make(map[string]time.Time)
}
// GetCacheStats returns statistics about the cache
func (r *LuaRouter) GetCacheStats() map[string]any {
var routeStats fastcache.Stats
var bytecodeStats fastcache.Stats
r.routeCache.UpdateStats(&routeStats)
r.bytecodeCache.UpdateStats(&bytecodeStats)
return map[string]any{
"routeEntries": routeStats.EntriesCount,
"routeBytes": routeStats.BytesSize,
"routeCollisions": routeStats.Collisions,
"bytecodeEntries": bytecodeStats.EntriesCount,
"bytecodeBytes": bytecodeStats.BytesSize,
"bytecodeCollisions": bytecodeStats.Collisions,
}
}

176
router/compile.go Normal file
View File

@ -0,0 +1,176 @@
package router
import (
"os"
"strings"
"time"
)
// compileWithMiddleware combines middleware and handler source, then compiles
func (r *LuaRouter) compileWithMiddleware(n *node, fsPath, scriptPath string) error {
if scriptPath == "" {
return nil
}
// Check if we need to recompile by comparing modification times
sourceKey := r.getSourceCacheKey(fsPath, scriptPath)
needsRecompile := false
// Check handler modification time
handlerInfo, err := os.Stat(scriptPath)
if err != nil {
n.err = err
return err
}
lastCompiled, exists := r.sourceMtimes[sourceKey]
if !exists || handlerInfo.ModTime().After(lastCompiled) {
needsRecompile = true
}
// Check middleware modification times
if !needsRecompile {
middlewareChain := r.getMiddlewareChain(fsPath)
for _, mwPath := range middlewareChain {
mwInfo, err := os.Stat(mwPath)
if err != nil {
n.err = err
return err
}
if mwInfo.ModTime().After(lastCompiled) {
needsRecompile = true
break
}
}
}
// Use cached bytecode if available and fresh
if !needsRecompile {
if bytecode, exists := r.sourceCache[sourceKey]; exists {
bytecodeKey := getBytecodeKey(scriptPath)
r.bytecodeCache.Set(bytecodeKey, bytecode)
return nil
}
}
// Build combined source
combinedSource, err := r.buildCombinedSource(fsPath, scriptPath)
if err != nil {
n.err = err
return err
}
// Compile combined source using shared state
r.compileStateMu.Lock()
bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath)
r.compileStateMu.Unlock()
if err != nil {
n.err = err
return err
}
// Cache everything
bytecodeKey := getBytecodeKey(scriptPath)
r.bytecodeCache.Set(bytecodeKey, bytecode)
r.sourceCache[sourceKey] = bytecode
r.sourceMtimes[sourceKey] = time.Now()
n.err = nil
return nil
}
// buildCombinedSource builds the combined middleware + handler source
func (r *LuaRouter) buildCombinedSource(fsPath, scriptPath string) (string, error) {
var combinedSource strings.Builder
// Get middleware chain using filesystem path
middlewareChain := r.getMiddlewareChain(fsPath)
// Add middleware in order
for _, mwPath := range middlewareChain {
content, err := r.getFileContent(mwPath)
if err != nil {
return "", err
}
combinedSource.WriteString("-- Middleware: ")
combinedSource.WriteString(mwPath)
combinedSource.WriteString("\n")
combinedSource.Write(content)
combinedSource.WriteString("\n")
}
// Add main handler
content, err := r.getFileContent(scriptPath)
if err != nil {
return "", err
}
combinedSource.WriteString("-- Handler: ")
combinedSource.WriteString(scriptPath)
combinedSource.WriteString("\n")
combinedSource.Write(content)
return combinedSource.String(), nil
}
// getFileContent reads file content with caching
func (r *LuaRouter) getFileContent(path string) ([]byte, error) {
// Check cache first
if content, exists := r.middlewareCache[path]; exists {
// Verify file hasn't changed
info, err := os.Stat(path)
if err == nil {
if cachedTime, exists := r.sourceMtimes[path]; exists && !info.ModTime().After(cachedTime) {
return content, nil
}
}
}
// Read from disk
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Cache it
r.middlewareCache[path] = content
r.sourceMtimes[path] = time.Now()
return content, nil
}
// getSourceCacheKey generates a unique key for combined source
func (r *LuaRouter) getSourceCacheKey(fsPath, scriptPath string) string {
middlewareChain := r.getMiddlewareChain(fsPath)
var keyParts []string
keyParts = append(keyParts, middlewareChain...)
keyParts = append(keyParts, scriptPath)
return strings.Join(keyParts, "|")
}
// getMiddlewareChain returns middleware files that apply to the given filesystem path
func (r *LuaRouter) getMiddlewareChain(fsPath string) []string {
var chain []string
// Collect middleware from root to specific path using filesystem path (includes groups)
pathParts := strings.Split(strings.Trim(fsPath, "/"), "/")
if pathParts[0] == "" {
pathParts = []string{}
}
// Add root middleware
if mw, exists := r.middlewareFiles["/"]; exists {
chain = append(chain, mw...)
}
// Add middleware from each path level (including groups)
currentPath := ""
for _, part := range pathParts {
currentPath += "/" + part
if mw, exists := r.middlewareFiles[currentPath]; exists {
chain = append(chain, mw...)
}
}
return chain
}

View File

@ -1,4 +1,4 @@
package routers
package router
import "errors"

187
router/match.go Normal file
View File

@ -0,0 +1,187 @@
package router
import (
"os"
"strings"
)
// Match finds a handler for the given method and path (URL path, excludes groups)
func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) {
params.Count = 0
r.mu.RLock()
root, exists := r.routes[method]
r.mu.RUnlock()
if !exists {
return nil, false
}
segments := strings.Split(strings.Trim(path, "/"), "/")
return r.matchPath(root, segments, params, 0)
}
// matchPath recursively matches a path against the routing tree
func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, depth int) (*node, bool) {
// Filter empty segments
filteredSegments := segments[:0]
for _, segment := range segments {
if segment != "" {
filteredSegments = append(filteredSegments, segment)
}
}
segments = filteredSegments
if len(segments) == 0 {
if current.handler != "" {
return current, true
}
if current.indexFile != "" {
return current, true
}
return nil, false
}
segment := segments[0]
remaining := segments[1:]
// Try static child first
if child, exists := current.staticChild[segment]; exists {
if node, found := r.matchPath(child, remaining, params, depth+1); found {
return node, true
}
}
// Try parameter child
if current.paramChild != nil {
if params.Count < maxParams {
params.Keys[params.Count] = current.paramChild.paramName
params.Values[params.Count] = segment
params.Count++
}
if node, found := r.matchPath(current.paramChild, remaining, params, depth+1); found {
return node, true
}
params.Count--
}
// Fall back to index.lua
if current.indexFile != "" {
return current, true
}
return nil, false
}
// GetRouteInfo returns bytecode, script path, error, params, and found status
func (r *LuaRouter) GetRouteInfo(method, path []byte) ([]byte, string, error, *Params, bool) {
// Convert to string for internal processing
methodStr := string(method)
pathStr := string(path)
routeCacheKey := getCacheKey(methodStr, pathStr)
routeCacheData := r.routeCache.Get(nil, routeCacheKey)
// Fast path: found in cache
if len(routeCacheData) > 0 {
handlerPath := string(routeCacheData[8:])
bytecodeKey := routeCacheData[:8]
bytecode := r.bytecodeCache.Get(nil, bytecodeKey)
n, exists := r.nodeForHandler(handlerPath)
if !exists {
r.routeCache.Del(routeCacheKey)
return nil, "", nil, nil, false
}
// Check if recompilation needed
if len(bytecode) > 0 {
// For cached routes, we need to re-match to get params
params := &Params{}
r.Match(methodStr, pathStr, params)
return bytecode, handlerPath, n.err, params, true
}
// Recompile if needed
fileInfo, err := os.Stat(handlerPath)
if err != nil || fileInfo.ModTime().After(n.modTime) {
scriptPath := n.handler
if scriptPath == "" {
scriptPath = n.indexFile
}
fsPath := n.fsPath
if fsPath == "" {
fsPath = "/"
}
if err := r.compileWithMiddleware(n, fsPath, scriptPath); err != nil {
params := &Params{}
r.Match(methodStr, pathStr, params)
return nil, handlerPath, n.err, params, true
}
newBytecodeKey := getBytecodeKey(handlerPath)
bytecode = r.bytecodeCache.Get(nil, newBytecodeKey)
newCacheData := make([]byte, 8+len(handlerPath))
copy(newCacheData[:8], newBytecodeKey)
copy(newCacheData[8:], handlerPath)
r.routeCache.Set(routeCacheKey, newCacheData)
params := &Params{}
r.Match(methodStr, pathStr, params)
return bytecode, handlerPath, n.err, params, true
}
params := &Params{}
r.Match(methodStr, pathStr, params)
return bytecode, handlerPath, n.err, params, true
}
// Slow path: lookup and compile
params := &Params{}
node, found := r.Match(methodStr, pathStr, params)
if !found {
return nil, "", nil, nil, false
}
scriptPath := node.handler
if scriptPath == "" && node.indexFile != "" {
scriptPath = node.indexFile
}
if scriptPath == "" {
return nil, "", nil, nil, false
}
bytecodeKey := getBytecodeKey(scriptPath)
bytecode := r.bytecodeCache.Get(nil, bytecodeKey)
if len(bytecode) == 0 {
fsPath := node.fsPath
if fsPath == "" {
fsPath = "/"
}
if err := r.compileWithMiddleware(node, fsPath, scriptPath); err != nil {
return nil, scriptPath, node.err, params, true
}
bytecode = r.bytecodeCache.Get(nil, bytecodeKey)
}
// Cache the route
cacheData := make([]byte, 8+len(scriptPath))
copy(cacheData[:8], bytecodeKey)
copy(cacheData[8:], scriptPath)
r.routeCache.Set(routeCacheKey, cacheData)
return bytecode, scriptPath, node.err, params, true
}
// GetRouteInfoString is a convenience method that accepts strings
func (r *LuaRouter) GetRouteInfoString(method, path string) ([]byte, string, error, *Params, bool) {
return r.GetRouteInfo([]byte(method), []byte(path))
}

190
router/node.go Normal file
View File

@ -0,0 +1,190 @@
package router
import (
"path/filepath"
"strings"
"time"
)
// node represents a node in the radix trie
type node struct {
// Static children mapped by path segment
staticChild map[string]*node
// Parameter child for dynamic segments (e.g., :id)
paramChild *node
paramName string
// Handler information
handler string // Path to the handler file
indexFile string // Path to index.lua if exists
modTime time.Time // Modification time of the handler
fsPath string // Filesystem path (includes groups)
// Compilation error if any
err error
}
// pathInfo holds both URL path and filesystem path
type pathInfo struct {
urlPath string // URL path without groups (e.g., /users)
fsPath string // Filesystem path with groups (e.g., /(admin)/users)
}
// parsePathWithGroups parses a filesystem path, extracting groups
func parsePathWithGroups(fsPath string) *pathInfo {
segments := strings.Split(strings.Trim(fsPath, "/"), "/")
var urlSegments []string
for _, segment := range segments {
if segment == "" {
continue
}
// Skip group segments (enclosed in parentheses)
if strings.HasPrefix(segment, "(") && strings.HasSuffix(segment, ")") {
continue
}
urlSegments = append(urlSegments, segment)
}
urlPath := "/"
if len(urlSegments) > 0 {
urlPath = "/" + strings.Join(urlSegments, "/")
}
return &pathInfo{
urlPath: urlPath,
fsPath: fsPath,
}
}
// findOrCreateNode finds or creates a node at the given URL path
func (r *LuaRouter) findOrCreateNode(root *node, urlPath string) *node {
segments := strings.Split(strings.Trim(urlPath, "/"), "/")
if len(segments) == 1 && segments[0] == "" {
return root
}
current := root
for _, segment := range segments {
if segment == "" {
continue
}
// Check if it's a parameter
if strings.HasPrefix(segment, ":") {
paramName := segment[1:]
if current.paramChild == nil {
current.paramChild = &node{
staticChild: make(map[string]*node),
paramName: paramName,
}
}
current = current.paramChild
} else {
// Static segment
if _, exists := current.staticChild[segment]; !exists {
current.staticChild[segment] = &node{
staticChild: make(map[string]*node),
}
}
current = current.staticChild[segment]
}
}
return current
}
// addRoute adds a route to the tree
func (r *LuaRouter) addRoute(root *node, pathInfo *pathInfo, handlerPath string, modTime time.Time) {
node := r.findOrCreateNode(root, pathInfo.urlPath)
node.handler = handlerPath
node.modTime = modTime
node.fsPath = pathInfo.fsPath
// Compile the route with middleware
r.compileWithMiddleware(node, pathInfo.fsPath, handlerPath)
// Track failed routes
if node.err != nil {
key := filepath.Base(handlerPath) + ":" + pathInfo.urlPath
r.failedRoutes[key] = &RouteError{
Path: pathInfo.urlPath,
Method: strings.ToUpper(strings.TrimSuffix(filepath.Base(handlerPath), ".lua")),
ScriptPath: handlerPath,
Err: node.err,
}
}
}
// 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 recursively searches for a node with the given handler path
func findNodeByHandler(n *node, handlerPath string) *node {
if n.handler == handlerPath || n.indexFile == handlerPath {
return n
}
// Search static children
for _, child := range n.staticChild {
if found := findNodeByHandler(child, handlerPath); found != nil {
return found
}
}
// Search param child
if n.paramChild != nil {
if found := findNodeByHandler(n.paramChild, handlerPath); found != nil {
return found
}
}
return nil
}
// countNodesAndBytecode counts nodes and bytecode size in the tree
func countNodesAndBytecode(n *node) (int, int64) {
if n == nil {
return 0, 0
}
count := 0
bytes := int64(0)
// Count this node if it has a handler
if n.handler != "" || n.indexFile != "" {
count = 1
// Estimate bytecode size (would need actual bytecode cache lookup for accuracy)
bytes = 1024 // Placeholder
}
// Count static children
for _, child := range n.staticChild {
c, b := countNodesAndBytecode(child)
count += c
bytes += b
}
// Count param child
if n.paramChild != nil {
c, b := countNodesAndBytecode(n.paramChild)
count += c
bytes += b
}
return count, bytes
}

44
router/params.go Normal file
View File

@ -0,0 +1,44 @@
package router
// Maximum number of URL parameters per route
const maxParams = 20
// Params holds URL parameters with fixed-size arrays to avoid allocations
type Params struct {
Keys [maxParams]string
Values [maxParams]string
Count int
}
// Get returns a parameter value by name
func (p *Params) Get(name string) string {
for i := range p.Count {
if p.Keys[i] == name {
return p.Values[i]
}
}
return ""
}
// Reset clears all parameters
func (p *Params) Reset() {
p.Count = 0
}
// Set adds or updates a parameter
func (p *Params) Set(name, value string) {
// Try to update existing
for i := range p.Count {
if p.Keys[i] == name {
p.Values[i] = value
return
}
}
// Add new if space available
if p.Count < maxParams {
p.Keys[p.Count] = name
p.Values[p.Count] = value
p.Count++
}
}

156
router/router.go Normal file
View File

@ -0,0 +1,156 @@
package router
import (
"errors"
"os"
"sync"
"time"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"github.com/VictoriaMetrics/fastcache"
)
// Default cache sizes
const (
defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache
defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache
)
// 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
routeCache *fastcache.Cache // Cache for route lookups
bytecodeCache *fastcache.Cache // Cache for compiled bytecode
// Middleware tracking for path hierarchy
middlewareFiles map[string][]string // filesystem path -> middleware file paths
// Caching fields
middlewareCache map[string][]byte // path -> content
sourceCache map[string][]byte // combined source cache key -> compiled bytecode
sourceMtimes map[string]time.Time // track modification times
// Shared Lua state for compilation
compileState *luajit.State
compileStateMu sync.Mutex // Protect concurrent access to Lua state
}
// NewLuaRouter creates a new LuaRouter instance
func NewLuaRouter(routesDir string) (*LuaRouter, error) {
info, err := os.Stat(routesDir)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, errors.New("routes path is not a directory")
}
// Create shared Lua state
compileState := luajit.New()
if compileState == nil {
return nil, errors.New("failed to create Lua compile state")
}
r := &LuaRouter{
routesDir: routesDir,
routes: make(map[string]*node),
failedRoutes: make(map[string]*RouteError),
middlewareFiles: make(map[string][]string),
routeCache: fastcache.New(defaultRouteMaxBytes),
bytecodeCache: fastcache.New(defaultBytecodeMaxBytes),
middlewareCache: make(map[string][]byte),
sourceCache: make(map[string][]byte),
sourceMtimes: make(map[string]time.Time),
compileState: compileState,
}
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
for _, method := range methods {
r.routes[method] = &node{
staticChild: make(map[string]*node),
}
}
err = r.buildRoutes()
if len(r.failedRoutes) > 0 {
return r, ErrRoutesCompilationErrors
}
return r, err
}
// Refresh rebuilds the router by rescanning the routes directory
func (r *LuaRouter) Refresh() error {
r.mu.Lock()
defer r.mu.Unlock()
for method := range r.routes {
r.routes[method] = &node{
staticChild: make(map[string]*node),
}
}
r.failedRoutes = make(map[string]*RouteError)
r.middlewareFiles = make(map[string][]string)
r.middlewareCache = make(map[string][]byte)
r.sourceCache = make(map[string][]byte)
r.sourceMtimes = make(map[string]time.Time)
err := r.buildRoutes()
if len(r.failedRoutes) > 0 {
return ErrRoutesCompilationErrors
}
return err
}
// ReportFailedRoutes returns a list of routes that failed to compile
func (r *LuaRouter) ReportFailedRoutes() []*RouteError {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]*RouteError, 0, len(r.failedRoutes))
for _, re := range r.failedRoutes {
result = append(result, re)
}
return result
}
// Close cleans up the router and its resources
func (r *LuaRouter) Close() {
r.compileStateMu.Lock()
if r.compileState != nil {
r.compileState.Close()
r.compileState = nil
}
r.compileStateMu.Unlock()
}
// GetRouteStats returns statistics about the router
func (r *LuaRouter) GetRouteStats() (int, int64) {
r.mu.RLock()
defer r.mu.RUnlock()
routeCount := 0
bytecodeBytes := int64(0)
for _, root := range r.routes {
count, bytes := countNodesAndBytecode(root)
routeCount += count
bytecodeBytes += bytes
}
return routeCount, bytecodeBytes
}
type NodeWithError struct {
ScriptPath string
Error error
}

View File

@ -1,827 +0,0 @@
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
// 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 {
Keys [maxParams]string
Values [maxParams]string
Count int
}
// Get returns a parameter value by name
func (p *Params) Get(name string) string {
for i := range p.Count {
if p.Keys[i] == name {
return p.Values[i]
}
}
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
routeCache *fastcache.Cache // Cache for route lookups
bytecodeCache *fastcache.Cache // Cache for compiled bytecode
// Middleware tracking for path hierarchy
middlewareFiles map[string][]string // filesystem path -> middleware file paths
// New caching fields
middlewareCache map[string][]byte // path -> content
sourceCache map[string][]byte // combined source cache key -> compiled bytecode
sourceMtimes map[string]time.Time // track modification times
// Shared Lua state for compilation
compileState *luajit.State
compileStateMu sync.Mutex // Protect concurrent access to Lua state
}
// node represents a node in the routing trie
type node struct {
handler string // Path to Lua file (empty if not an endpoint)
indexFile string // Path to index.lua file (catch-all)
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
fsPath string // Original filesystem path (includes groups)
}
// pathInfo holds both filesystem and URL path information
type pathInfo struct {
fsPath string // Filesystem path (includes groups)
urlPath string // URL path (excludes groups)
}
// parsePathWithGroups separates filesystem path from URL path
func parsePathWithGroups(fsPath string) pathInfo {
segments := strings.Split(strings.Trim(fsPath, "/"), "/")
var urlSegments []string
for _, segment := range segments {
if segment == "" {
continue
}
// Skip group segments for URL path
if len(segment) >= 3 && segment[0] == '(' && segment[len(segment)-1] == ')' {
continue
}
urlSegments = append(urlSegments, segment)
}
urlPath := "/"
if len(urlSegments) > 0 {
urlPath = "/" + strings.Join(urlSegments, "/")
}
return pathInfo{
fsPath: fsPath,
urlPath: urlPath,
}
}
// NewLuaRouter creates a new LuaRouter instance
func NewLuaRouter(routesDir string) (*LuaRouter, error) {
info, err := os.Stat(routesDir)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, errors.New("routes path is not a directory")
}
// Create shared Lua state
compileState := luajit.New()
if compileState == nil {
return nil, errors.New("failed to create Lua compile state")
}
r := &LuaRouter{
routesDir: routesDir,
routes: make(map[string]*node),
failedRoutes: make(map[string]*RouteError),
middlewareFiles: make(map[string][]string),
routeCache: fastcache.New(defaultRouteMaxBytes),
bytecodeCache: fastcache.New(defaultBytecodeMaxBytes),
middlewareCache: make(map[string][]byte),
sourceCache: make(map[string][]byte),
sourceMtimes: make(map[string]time.Time),
compileState: compileState,
}
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
for _, method := range methods {
r.routes[method] = &node{
staticChild: make(map[string]*node),
}
}
err = r.buildRoutes()
if len(r.failedRoutes) > 0 {
return r, ErrRoutesCompilationErrors
}
return r, err
}
// buildRoutes scans the routes directory and builds the routing tree
func (r *LuaRouter) buildRoutes() error {
r.failedRoutes = make(map[string]*RouteError)
r.middlewareFiles = make(map[string][]string)
// First pass: collect all middleware files
err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err
}
if strings.TrimSuffix(info.Name(), ".lua") == "middleware" {
relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path))
if err != nil {
return err
}
fsPath := "/"
if relDir != "." {
fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/")
}
// Use filesystem path for middleware (includes groups)
r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path)
}
return nil
})
if err != nil {
return err
}
// Second pass: build routes with combined middleware + handler
return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err
}
fileName := strings.TrimSuffix(info.Name(), ".lua")
// Skip middleware files (already processed)
if fileName == "middleware" {
return nil
}
relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path))
if err != nil {
return err
}
fsPath := "/"
if relDir != "." {
fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/")
}
pathInfo := parsePathWithGroups(fsPath)
// Handle index.lua files
if fileName == "index" {
for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} {
root := r.routes[method]
node := r.findOrCreateNode(root, pathInfo.urlPath)
node.indexFile = path
node.modTime = info.ModTime()
node.fsPath = pathInfo.fsPath
r.compileWithMiddleware(node, pathInfo.fsPath, path)
}
return nil
}
// Handle method files
method := strings.ToUpper(fileName)
root, exists := r.routes[method]
if !exists {
return nil
}
r.addRoute(root, pathInfo, path, info.ModTime())
return nil
})
}
// addRoute adds a route to the routing tree
func (r *LuaRouter) addRoute(root *node, pathInfo pathInfo, handlerPath string, modTime time.Time) error {
segments := strings.Split(strings.Trim(pathInfo.urlPath, "/"), "/")
current := root
for _, segment := range segments {
if segment == "" {
continue
}
if len(segment) >= 2 && segment[0] == '[' && segment[len(segment)-1] == ']' {
if current.paramChild == nil {
current.paramChild = &node{
paramName: segment[1 : len(segment)-1],
staticChild: make(map[string]*node),
}
}
current = current.paramChild
} else {
child, exists := current.staticChild[segment]
if !exists {
child = &node{
staticChild: make(map[string]*node),
}
current.staticChild[segment] = child
}
current = child
}
}
current.handler = handlerPath
current.modTime = modTime
current.fsPath = pathInfo.fsPath
return r.compileWithMiddleware(current, pathInfo.fsPath, handlerPath)
}
// compileWithMiddleware combines middleware and handler source, then compiles
func (r *LuaRouter) compileWithMiddleware(n *node, fsPath, scriptPath string) error {
if scriptPath == "" {
return nil
}
// Check if we need to recompile by comparing modification times
sourceKey := r.getSourceCacheKey(fsPath, scriptPath)
needsRecompile := false
// Check handler modification time
handlerInfo, err := os.Stat(scriptPath)
if err != nil {
n.err = err
return err
}
lastCompiled, exists := r.sourceMtimes[sourceKey]
if !exists || handlerInfo.ModTime().After(lastCompiled) {
needsRecompile = true
}
// Check middleware modification times
if !needsRecompile {
middlewareChain := r.getMiddlewareChain(fsPath)
for _, mwPath := range middlewareChain {
mwInfo, err := os.Stat(mwPath)
if err != nil {
n.err = err
return err
}
if mwInfo.ModTime().After(lastCompiled) {
needsRecompile = true
break
}
}
}
// Use cached bytecode if available and fresh
if !needsRecompile {
if bytecode, exists := r.sourceCache[sourceKey]; exists {
bytecodeKey := getBytecodeKey(scriptPath)
r.bytecodeCache.Set(bytecodeKey, bytecode)
return nil
}
}
// Build combined source
combinedSource, err := r.buildCombinedSource(fsPath, scriptPath)
if err != nil {
n.err = err
return err
}
// Compile combined source using shared state
r.compileStateMu.Lock()
bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath)
r.compileStateMu.Unlock()
if err != nil {
n.err = err
return err
}
// Cache everything
bytecodeKey := getBytecodeKey(scriptPath)
r.bytecodeCache.Set(bytecodeKey, bytecode)
r.sourceCache[sourceKey] = bytecode
r.sourceMtimes[sourceKey] = time.Now()
n.err = nil
return nil
}
// buildCombinedSource builds the combined middleware + handler source
func (r *LuaRouter) buildCombinedSource(fsPath, scriptPath string) (string, error) {
var combinedSource strings.Builder
// Get middleware chain using filesystem path
middlewareChain := r.getMiddlewareChain(fsPath)
// Add middleware in order
for _, mwPath := range middlewareChain {
content, err := r.getFileContent(mwPath)
if err != nil {
return "", err
}
combinedSource.WriteString("-- Middleware: ")
combinedSource.WriteString(mwPath)
combinedSource.WriteString("\n")
combinedSource.Write(content)
combinedSource.WriteString("\n")
}
// Add main handler
content, err := r.getFileContent(scriptPath)
if err != nil {
return "", err
}
combinedSource.WriteString("-- Handler: ")
combinedSource.WriteString(scriptPath)
combinedSource.WriteString("\n")
combinedSource.Write(content)
return combinedSource.String(), nil
}
// getFileContent reads file content with caching
func (r *LuaRouter) getFileContent(path string) ([]byte, error) {
// Check cache first
if content, exists := r.middlewareCache[path]; exists {
// Verify file hasn't changed
info, err := os.Stat(path)
if err == nil {
if cachedTime, exists := r.sourceMtimes[path]; exists && !info.ModTime().After(cachedTime) {
return content, nil
}
}
}
// Read from disk
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Cache it
r.middlewareCache[path] = content
r.sourceMtimes[path] = time.Now()
return content, nil
}
// getSourceCacheKey generates a unique key for combined source
func (r *LuaRouter) getSourceCacheKey(fsPath, scriptPath string) string {
middlewareChain := r.getMiddlewareChain(fsPath)
var keyParts []string
keyParts = append(keyParts, middlewareChain...)
keyParts = append(keyParts, scriptPath)
return strings.Join(keyParts, "|")
}
// getMiddlewareChain returns middleware files that apply to the given filesystem path
func (r *LuaRouter) getMiddlewareChain(fsPath string) []string {
var chain []string
// Collect middleware from root to specific path using filesystem path (includes groups)
pathParts := strings.Split(strings.Trim(fsPath, "/"), "/")
if pathParts[0] == "" {
pathParts = []string{}
}
// Add root middleware
if mw, exists := r.middlewareFiles["/"]; exists {
chain = append(chain, mw...)
}
// Add middleware from each path level (including groups)
currentPath := ""
for _, part := range pathParts {
currentPath += "/" + part
if mw, exists := r.middlewareFiles[currentPath]; exists {
chain = append(chain, mw...)
}
}
return chain
}
// findOrCreateNode finds or creates a node at the given URL path (excludes groups)
func (r *LuaRouter) findOrCreateNode(root *node, urlPath string) *node {
segments := strings.Split(strings.Trim(urlPath, "/"), "/")
current := root
for _, segment := range segments {
if segment == "" {
continue
}
if len(segment) >= 2 && segment[0] == '[' && segment[len(segment)-1] == ']' {
if current.paramChild == nil {
current.paramChild = &node{
paramName: segment[1 : len(segment)-1],
staticChild: make(map[string]*node),
}
}
current = current.paramChild
} else {
child, exists := current.staticChild[segment]
if !exists {
child = &node{
staticChild: make(map[string]*node),
}
current.staticChild[segment] = child
}
current = child
}
}
return current
}
// getRouteKey generates a unique key for a route
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
}
// getCacheKey generates a cache key for a method and path
func getCacheKey(method, path string) []byte {
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 (URL path, excludes groups)
func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) {
params.Count = 0
r.mu.RLock()
root, exists := r.routes[method]
r.mu.RUnlock()
if !exists {
return nil, false
}
segments := strings.Split(strings.Trim(path, "/"), "/")
return r.matchPath(root, segments, params, 0)
}
// matchPath recursively matches a path against the routing tree
func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, depth int) (*node, bool) {
// Filter empty segments
filteredSegments := segments[:0]
for _, segment := range segments {
if segment != "" {
filteredSegments = append(filteredSegments, segment)
}
}
segments = filteredSegments
if len(segments) == 0 {
if current.handler != "" {
return current, true
}
if current.indexFile != "" {
return current, true
}
return nil, false
}
segment := segments[0]
remaining := segments[1:]
// Try static child first
if child, exists := current.staticChild[segment]; exists {
if node, found := r.matchPath(child, remaining, params, depth+1); found {
return node, true
}
}
// Try parameter child
if current.paramChild != nil {
if params.Count < maxParams {
params.Keys[params.Count] = current.paramChild.paramName
params.Values[params.Count] = segment
params.Count++
}
if node, found := r.matchPath(current.paramChild, remaining, params, depth+1); found {
return node, true
}
params.Count--
}
// Fall back to index.lua
if current.indexFile != "" {
return current, true
}
return nil, false
}
// GetRouteInfo returns the combined bytecode, script path, and any error
func (r *LuaRouter) GetRouteInfo(method, path string, params *Params) ([]byte, string, error, bool) {
routeCacheKey := getCacheKey(method, path)
routeCacheData := r.routeCache.Get(nil, routeCacheKey)
if len(routeCacheData) > 0 {
handlerPath := string(routeCacheData[8:])
bytecodeKey := routeCacheData[:8]
bytecode := r.bytecodeCache.Get(nil, bytecodeKey)
n, exists := r.nodeForHandler(handlerPath)
if !exists {
r.routeCache.Del(routeCacheKey)
return nil, "", nil, false
}
if len(bytecode) > 0 {
return bytecode, handlerPath, n.err, true
}
fileInfo, err := os.Stat(handlerPath)
if err != nil || fileInfo.ModTime().After(n.modTime) {
scriptPath := n.handler
if scriptPath == "" {
scriptPath = n.indexFile
}
fsPath := n.fsPath
if fsPath == "" {
fsPath = "/"
}
if err := r.compileWithMiddleware(n, fsPath, scriptPath); err != nil {
return nil, handlerPath, n.err, true
}
newBytecodeKey := getBytecodeKey(handlerPath)
bytecode = r.bytecodeCache.Get(nil, newBytecodeKey)
newCacheData := make([]byte, 8+len(handlerPath))
copy(newCacheData[:8], newBytecodeKey)
copy(newCacheData[8:], handlerPath)
r.routeCache.Set(routeCacheKey, newCacheData)
return bytecode, handlerPath, n.err, true
}
return bytecode, handlerPath, n.err, true
}
node, found := r.Match(method, path, params)
if !found {
return nil, "", nil, false
}
scriptPath := node.handler
if scriptPath == "" && node.indexFile != "" {
scriptPath = node.indexFile
}
if scriptPath == "" {
return nil, "", nil, false
}
bytecodeKey := getBytecodeKey(scriptPath)
bytecode := r.bytecodeCache.Get(nil, bytecodeKey)
if len(bytecode) == 0 {
fsPath := node.fsPath
if fsPath == "" {
fsPath = "/"
}
if err := r.compileWithMiddleware(node, fsPath, scriptPath); err != nil {
return nil, scriptPath, node.err, true
}
bytecode = r.bytecodeCache.Get(nil, bytecodeKey)
}
cacheData := make([]byte, 8+len(scriptPath))
copy(cacheData[:8], bytecodeKey)
copy(cacheData[8:], scriptPath)
r.routeCache.Set(routeCacheKey, cacheData)
return bytecode, scriptPath, node.err, 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 || current.indexFile == handlerPath {
return current
}
for _, child := range current.staticChild {
if node := findNodeByHandler(child, handlerPath); node != nil {
return node
}
}
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
func (r *LuaRouter) Refresh() error {
r.mu.Lock()
defer r.mu.Unlock()
for method := range r.routes {
r.routes[method] = &node{
staticChild: make(map[string]*node),
}
}
r.failedRoutes = make(map[string]*RouteError)
r.middlewareFiles = make(map[string][]string)
r.middlewareCache = make(map[string][]byte)
r.sourceCache = make(map[string][]byte)
r.sourceMtimes = make(map[string]time.Time)
err := r.buildRoutes()
if len(r.failedRoutes) > 0 {
return ErrRoutesCompilationErrors
}
return err
}
// ReportFailedRoutes returns a list of routes that failed to compile
func (r *LuaRouter) ReportFailedRoutes() []*RouteError {
r.mu.RLock()
defer r.mu.RUnlock()
result := make([]*RouteError, 0, len(r.failedRoutes))
for _, re := range r.failedRoutes {
result = append(result, re)
}
return result
}
// ClearCache clears all caches
func (r *LuaRouter) ClearCache() {
r.routeCache.Reset()
r.bytecodeCache.Reset()
r.middlewareCache = make(map[string][]byte)
r.sourceCache = make(map[string][]byte)
r.sourceMtimes = make(map[string]time.Time)
}
// Close cleans up the router and its resources
func (r *LuaRouter) Close() {
r.compileStateMu.Lock()
if r.compileState != nil {
r.compileState.Close()
r.compileState = nil
}
r.compileStateMu.Unlock()
}
// GetCacheStats returns statistics about the cache
func (r *LuaRouter) GetCacheStats() map[string]any {
var routeStats fastcache.Stats
var bytecodeStats fastcache.Stats
r.routeCache.UpdateStats(&routeStats)
r.bytecodeCache.UpdateStats(&bytecodeStats)
return map[string]any{
"routeEntries": routeStats.EntriesCount,
"routeBytes": routeStats.BytesSize,
"routeCollisions": routeStats.Collisions,
"bytecodeEntries": bytecodeStats.EntriesCount,
"bytecodeBytes": bytecodeStats.BytesSize,
"bytecodeCollisions": bytecodeStats.Collisions,
}
}
// GetRouteStats returns statistics about the router
func (r *LuaRouter) GetRouteStats() (int, int64) {
r.mu.RLock()
defer r.mu.RUnlock()
routeCount := 0
bytecodeBytes := int64(0)
for _, root := range r.routes {
count, bytes := countNodesAndBytecode(root)
routeCount += count
bytecodeBytes += bytes
}
return routeCount, bytecodeBytes
}
// countNodesAndBytecode traverses the tree and counts nodes and bytecode size
func countNodesAndBytecode(n *node) (count int, bytecodeBytes int64) {
if n == nil {
return 0, 0
}
if n.handler != "" || n.indexFile != "" {
count = 1
bytecodeBytes = 2048
}
for _, child := range n.staticChild {
childCount, childBytes := countNodesAndBytecode(child)
count += childCount
bytecodeBytes += childBytes
}
if n.paramChild != nil {
childCount, childBytes := countNodesAndBytecode(n.paramChild)
count += childCount
bytecodeBytes += childBytes
}
return count, bytecodeBytes
}
type NodeWithError struct {
ScriptPath string
Error error
}

View File

@ -1,160 +0,0 @@
package routers
import (
"errors"
"io/fs"
"os"
"path/filepath"
"strings"
"time"
"Moonshark/utils/logger"
"github.com/valyala/fasthttp"
)
// StaticRouter is a simplified router for static files using FastHTTP's built-in capabilities
type StaticRouter struct {
fs *fasthttp.FS
fsHandler fasthttp.RequestHandler
urlPrefix string
rootDir string
log bool
useBrotli bool
useZstd bool
}
// NewStaticRouter creates a new StaticRouter instance
func NewStaticRouter(rootDir string) (*StaticRouter, error) {
info, err := os.Stat(rootDir)
if err != nil {
return nil, err
}
if !info.IsDir() {
return nil, errors.New("root path is not a directory")
}
fs := &fasthttp.FS{
Root: rootDir,
IndexNames: []string{"index.html"},
GenerateIndexPages: false,
AcceptByteRange: true,
Compress: true,
CacheDuration: 24 * time.Hour,
CompressedFileSuffix: ".gz",
CompressBrotli: true,
CompressZstd: true,
}
r := &StaticRouter{
fs: fs,
urlPrefix: "",
rootDir: rootDir,
log: false,
useBrotli: true,
useZstd: true,
}
r.updatePathRewrite()
return r, nil
}
// WithEmbeddedFS sets an embedded filesystem instead of using the rootDir
func (r *StaticRouter) WithEmbeddedFS(embedded fs.FS) *StaticRouter {
r.fs.FS = embedded
r.fsHandler = r.fs.NewRequestHandler()
return r
}
// SetCompression configures the compression options
func (r *StaticRouter) SetCompression(useGzip, useBrotli, useZstd bool) {
r.fs.Compress = useGzip
r.fs.CompressBrotli = useBrotli
r.fs.CompressZstd = useZstd
r.useBrotli = useBrotli
r.useZstd = useZstd
r.fsHandler = r.fs.NewRequestHandler()
}
// SetCacheDuration sets the cache duration for HTTP headers
func (r *StaticRouter) SetCacheDuration(duration time.Duration) {
r.fs.CacheDuration = duration
r.fsHandler = r.fs.NewRequestHandler()
}
// SetURLPrefix sets the URL prefix for static assets
func (r *StaticRouter) SetURLPrefix(prefix string) {
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
r.urlPrefix = prefix
r.updatePathRewrite()
}
// updatePathRewrite updates the path rewriter based on the current prefix
func (r *StaticRouter) updatePathRewrite() {
r.fs.PathRewrite = fasthttp.NewPathPrefixStripper(len(r.urlPrefix))
r.fsHandler = r.fs.NewRequestHandler()
}
// EnableDebugLog enables debug logging
func (r *StaticRouter) EnableDebugLog() {
r.log = true
}
// DisableDebugLog disables debug logging
func (r *StaticRouter) DisableDebugLog() {
r.log = false
}
// ServeHTTP implements the http.Handler interface through fasthttpadaptor
func (r *StaticRouter) ServeHTTP(ctx *fasthttp.RequestCtx) {
path := string(ctx.Path())
if !strings.HasPrefix(path, r.urlPrefix) {
ctx.NotFound()
return
}
if r.log {
logger.Debug("[StaticRouter] Serving: %s", path)
}
r.fsHandler(ctx)
}
// Match finds a file path for the given URL path
func (r *StaticRouter) Match(urlPath string) (string, bool) {
if !strings.HasPrefix(urlPath, r.urlPrefix) {
return "", false
}
urlPath = strings.TrimPrefix(urlPath, r.urlPrefix)
if !strings.HasPrefix(urlPath, "/") {
urlPath = "/" + urlPath
}
filePath := filepath.Join(r.rootDir, filepath.FromSlash(strings.TrimPrefix(urlPath, "/")))
_, err := os.Stat(filePath)
if r.log && err == nil {
logger.Debug("[StaticRouter] MATCH: %s -> %s", urlPath, filePath)
}
return filePath, err == nil
}
// GetStats returns basic stats about the router
func (r *StaticRouter) GetStats() map[string]any {
return map[string]any{
"type": "StaticRouter",
"rootDir": r.rootDir,
"urlPrefix": r.urlPrefix,
"useBrotli": r.useBrotli,
"useZstd": r.useZstd,
"cacheTime": r.fs.CacheDuration.String(),
}
}

View File

@ -427,3 +427,8 @@ func deepEqual(a, b any) bool {
return false
}
// IsEmpty returns true if the session has no data
func (s *Session) IsEmpty() bool {
return len(s.Data) == 0
}

View File

@ -1,353 +0,0 @@
package tests
import (
"context"
"log"
"os"
"path/filepath"
"testing"
"Moonshark/routers"
"Moonshark/runner"
"Moonshark/utils/logger"
)
// setupTestEnv initializes test components and returns cleanup function
func setupTestEnv(b *testing.B) (*routers.LuaRouter, *runner.Runner, func()) {
// Completely silence all logging
originalStderr := os.Stderr
originalStdout := os.Stdout
devNull, _ := os.Open(os.DevNull)
// Redirect everything to devnull
os.Stderr = devNull
os.Stdout = devNull
log.SetOutput(devNull)
// Force reinit logger to be silent
logger.InitGlobalLogger(false, false)
// Create temp directories
tempDir, err := os.MkdirTemp("", "moonshark-bench")
if err != nil {
b.Fatalf("Failed to create temp dir: %v", err)
}
routesDir := filepath.Join(tempDir, "routes")
staticDir := filepath.Join(tempDir, "static")
libsDir := filepath.Join(tempDir, "libs")
dataDir := filepath.Join(tempDir, "data")
fsDir := filepath.Join(tempDir, "fs")
os.MkdirAll(routesDir, 0755)
os.MkdirAll(staticDir, 0755)
os.MkdirAll(libsDir, 0755)
os.MkdirAll(dataDir, 0755)
os.MkdirAll(fsDir, 0755)
// Create test routes
createTestRoutes(routesDir)
// Initialize router
luaRouter, err := routers.NewLuaRouter(routesDir)
if err != nil {
b.Fatalf("Failed to create router: %v", err)
}
// Initialize runner
luaRunner, err := runner.NewRunner(
runner.WithPoolSize(4),
runner.WithLibDirs(libsDir),
runner.WithDataDir(dataDir),
runner.WithFsDir(fsDir),
)
if err != nil {
b.Fatalf("Failed to create runner: %v", err)
}
// Return cleanup function that restores outputs
cleanup := func() {
luaRunner.Close()
os.RemoveAll(tempDir)
os.Stderr = originalStderr
os.Stdout = originalStdout
devNull.Close()
}
return luaRouter, luaRunner, cleanup
}
// createTestRoutes creates test Lua scripts for benchmarking
func createTestRoutes(routesDir string) {
// Simple GET endpoint
getCode := []byte(`return "Hello, World!"`)
os.WriteFile(filepath.Join(routesDir, "GET.lua"), getCode, 0644)
// POST endpoint with form handling
postCode := []byte(`
local data = ctx.form or {}
return "Received: " .. (data.message or "no message")
`)
os.WriteFile(filepath.Join(routesDir, "POST.lua"), postCode, 0644)
// Computationally intensive endpoint
complexDir := filepath.Join(routesDir, "complex")
os.MkdirAll(complexDir, 0755)
complexCode := []byte(`
local result = {}
for i = 1, 1000 do
table.insert(result, i * i)
end
return "Calculated " .. #result .. " squared numbers"
`)
os.WriteFile(filepath.Join(complexDir, "GET.lua"), complexCode, 0644)
// Create middleware for testing
middlewareCode := []byte(`
http.set_metadata("middleware_executed", true)
return nil
`)
os.WriteFile(filepath.Join(routesDir, "middleware.lua"), middlewareCode, 0644)
// Nested middleware
nestedDir := filepath.Join(routesDir, "api")
os.MkdirAll(nestedDir, 0755)
nestedMiddlewareCode := []byte(`
http.set_metadata("api_middleware", true)
return nil
`)
os.WriteFile(filepath.Join(nestedDir, "middleware.lua"), nestedMiddlewareCode, 0644)
// Nested endpoint
nestedEndpointCode := []byte(`return "API endpoint"`)
os.WriteFile(filepath.Join(nestedDir, "GET.lua"), nestedEndpointCode, 0644)
}
// BenchmarkRouterLookup tests route lookup performance
func BenchmarkRouterLookup(b *testing.B) {
luaRouter, _, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/"
params := &routers.Params{}
for b.Loop() {
_, _, _, _ = luaRouter.GetRouteInfo(method, path, params)
}
}
// BenchmarkSimpleLuaExecution tests execution of a simple Lua script
func BenchmarkSimpleLuaExecution(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/"
params := &routers.Params{}
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
ctx := runner.NewContext()
defer ctx.Release()
for b.Loop() {
_, _ = luaRunner.Run(bytecode, ctx, scriptPath)
}
}
// BenchmarkComplexLuaExecution tests execution of a computation-heavy script
func BenchmarkComplexLuaExecution(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/complex"
params := &routers.Params{}
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
ctx := runner.NewContext()
defer ctx.Release()
for b.Loop() {
_, _ = luaRunner.Run(bytecode, ctx, scriptPath)
}
}
// BenchmarkGetEndpoint tests end-to-end processing for GET endpoint
func BenchmarkGetEndpoint(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/"
params := &routers.Params{}
for b.Loop() {
// Route lookup
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
// Context setup
ctx := runner.NewContext()
// Script execution
_, _ = luaRunner.Run(bytecode, ctx, scriptPath)
// Cleanup
ctx.Release()
}
}
// BenchmarkPostEndpoint tests end-to-end processing for POST endpoint
func BenchmarkPostEndpoint(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "POST"
path := "/"
params := &routers.Params{}
for b.Loop() {
// Route lookup
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
// Context setup with form data
ctx := runner.NewContext()
ctx.Set("form", map[string]any{
"message": "Hello from benchmark test",
})
// Script execution
_, _ = luaRunner.Run(bytecode, ctx, scriptPath)
// Cleanup
ctx.Release()
}
}
// BenchmarkConcurrentExecution tests parallel execution performance
func BenchmarkConcurrentExecution(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/"
params := &routers.Params{}
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := runner.NewContext()
_, _ = luaRunner.Run(bytecode, ctx, scriptPath)
ctx.Release()
}
})
}
// BenchmarkConcurrentComplexExecution tests parallel execution of intensive scripts
func BenchmarkConcurrentComplexExecution(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/complex"
params := &routers.Params{}
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
ctx := runner.NewContext()
_, _ = luaRunner.Run(bytecode, ctx, scriptPath)
ctx.Release()
}
})
}
// BenchmarkMiddlewareExecution tests middleware + handler execution
func BenchmarkMiddlewareExecution(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/api"
params := &routers.Params{}
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
for b.Loop() {
ctx := runner.NewContext()
// Execute combined middleware + handler
response, _ := luaRunner.Run(bytecode, ctx, scriptPath)
if response != nil {
runner.ReleaseResponse(response)
}
ctx.Release()
}
}
// BenchmarkRouteCompilation tests the performance of route compilation
func BenchmarkRouteCompilation(b *testing.B) {
tempDir, err := os.MkdirTemp("", "moonshark-compile")
if err != nil {
b.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tempDir)
routesDir := filepath.Join(tempDir, "routes")
os.MkdirAll(routesDir, 0755)
for i := 0; i < b.N; i++ {
b.StopTimer()
os.RemoveAll(routesDir)
os.MkdirAll(routesDir, 0755)
createTestRoutes(routesDir)
b.StartTimer()
// Creating router triggers compilation
_, _ = routers.NewLuaRouter(routesDir)
}
}
// BenchmarkContextCreation measures the cost of creating execution contexts
func BenchmarkContextCreation(b *testing.B) {
for b.Loop() {
ctx := runner.NewContext()
ctx.Release()
}
}
// BenchmarkContextWithData measures context creation with realistic data
func BenchmarkContextWithData(b *testing.B) {
for i := 0; i < b.N; i++ {
ctx := runner.NewContext()
ctx.Set("method", "POST")
ctx.Set("path", "/api/users")
ctx.Set("host", "example.com")
ctx.Set("params", map[string]any{"id": "123"})
ctx.Set("form", map[string]any{
"username": "testuser",
"email": "user@example.com",
"active": true,
})
ctx.Release()
}
}
// BenchmarkRunnerExecute tests the runner's Execute method with timeout
func BenchmarkRunnerExecute(b *testing.B) {
luaRouter, luaRunner, cleanup := setupTestEnv(b)
defer cleanup()
method := "GET"
path := "/"
params := &routers.Params{}
bytecode, scriptPath, _, _ := luaRouter.GetRouteInfo(method, path, params)
for b.Loop() {
ctx := runner.NewContext()
_, _ = luaRunner.Execute(context.Background(), bytecode, ctx, scriptPath)
ctx.Release()
}
}

View File

@ -17,6 +17,7 @@ type Config struct {
Port int
Debug bool
HTTPLogging bool
StaticPrefix string
}
// Runner settings
@ -49,6 +50,7 @@ func New() *Config {
config.Server.Port = 3117
config.Server.Debug = false
config.Server.HTTPLogging = false
config.Server.StaticPrefix = "static/"
// Runner defaults
config.Runner.PoolSize = runtime.GOMAXPROCS(0)
@ -117,6 +119,9 @@ func applyConfig(config *Config, values map[string]any) {
if v, ok := serverTable["http_logging"].(bool); ok {
config.Server.HTTPLogging = v
}
if v, ok := serverTable["static_prefix"].(string); ok {
config.Server.StaticPrefix = v
}
}
// Apply runner settings

View File

@ -5,7 +5,7 @@ import (
"strings"
"sync"
"Moonshark/routers"
"Moonshark/router"
"Moonshark/runner"
"Moonshark/utils/color"
"Moonshark/utils/logger"
@ -34,7 +34,7 @@ func ShutdownWatcherManager() {
}
// WatchLuaRouter sets up a watcher for a LuaRouter's routes directory
func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) {
func WatchLuaRouter(router *router.LuaRouter, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) {
manager := GetWatcherManager()
config := DirectoryWatcherConfig{