various optimizations, static file serving changes, break down luarouter
This commit is contained in:
parent
163e94d576
commit
5b698f31e4
280
http/server.go
280
http/server.go
@ -1,11 +1,12 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"Moonshark/routers"
|
||||
"Moonshark/router"
|
||||
"Moonshark/runner"
|
||||
"Moonshark/sessions"
|
||||
"Moonshark/utils"
|
||||
@ -17,52 +18,113 @@ 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
|
||||
luaRunner *runner.Runner
|
||||
fasthttpServer *fasthttp.Server
|
||||
loggingEnabled bool
|
||||
debugMode bool
|
||||
config *config.Config
|
||||
sessionManager *sessions.SessionManager
|
||||
errorConfig utils.ErrorPageConfig
|
||||
ctxPool sync.Pool
|
||||
luaRouter *router.LuaRouter
|
||||
staticHandler fasthttp.RequestHandler
|
||||
staticFS *fasthttp.FS
|
||||
luaRunner *runner.Runner
|
||||
fasthttpServer *fasthttp.Server
|
||||
loggingEnabled bool
|
||||
debugMode bool
|
||||
config *config.Config
|
||||
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,
|
||||
luaRouter: luaRouter,
|
||||
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{
|
||||
Handler: s.handleRequest,
|
||||
Name: "Moonshark/" + metadata.Version,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
MaxRequestBodySize: 16 << 20,
|
||||
TCPKeepalive: true,
|
||||
TCPKeepalivePeriod: 60 * time.Second,
|
||||
ReduceMemoryUsage: true,
|
||||
DisablePreParseMultipartForm: true,
|
||||
Handler: s.handleRequest,
|
||||
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 {
|
||||
logger.Warning("Error parsing form: %v", err)
|
||||
if s.debugMode {
|
||||
logger.Warning("Error parsing form: %v", err)
|
||||
}
|
||||
luaCtx.Set("form", emptyMap)
|
||||
}
|
||||
} else {
|
||||
@ -172,23 +245,24 @@ 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
|
||||
}
|
||||
|
||||
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
|
||||
session.Clear()
|
||||
delete(response.SessionData, "__clear_all")
|
||||
}
|
||||
// Handle session updates
|
||||
if len(response.SessionData) > 0 {
|
||||
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
|
||||
session.Clear()
|
||||
delete(response.SessionData, "__clear_all")
|
||||
}
|
||||
|
||||
for k, v := range response.SessionData {
|
||||
if v == "__SESSION_DELETE_MARKER__" {
|
||||
session.Delete(k)
|
||||
} else {
|
||||
session.Set(k, v)
|
||||
for k, v := range response.SessionData {
|
||||
if v == "__SESSION_DELETE_MARKER__" {
|
||||
session.Delete(k)
|
||||
} else {
|
||||
session.Set(k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,6 +271,33 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
||||
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
41
main.go
@ -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
90
router/build.go
Normal 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
62
router/cache.go
Normal 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
176
router/compile.go
Normal 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
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package routers
|
||||
package router
|
||||
|
||||
import "errors"
|
||||
|
187
router/match.go
Normal file
187
router/match.go
Normal 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
190
router/node.go
Normal 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
44
router/params.go
Normal 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
156
router/router.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
@ -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(),
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -14,9 +14,10 @@ import (
|
||||
type Config struct {
|
||||
// Server settings
|
||||
Server struct {
|
||||
Port int
|
||||
Debug bool
|
||||
HTTPLogging bool
|
||||
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
|
||||
|
@ -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{
|
||||
|
Loading…
x
Reference in New Issue
Block a user