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
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"Moonshark/routers"
|
"Moonshark/router"
|
||||||
"Moonshark/runner"
|
"Moonshark/runner"
|
||||||
"Moonshark/sessions"
|
"Moonshark/sessions"
|
||||||
"Moonshark/utils"
|
"Moonshark/utils"
|
||||||
@ -17,52 +18,113 @@ import (
|
|||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
//methodGET = []byte("GET")
|
||||||
|
methodPOST = []byte("POST")
|
||||||
|
methodPUT = []byte("PUT")
|
||||||
|
methodPATCH = []byte("PATCH")
|
||||||
|
debugPath = []byte("/debug/stats")
|
||||||
|
)
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
luaRouter *routers.LuaRouter
|
luaRouter *router.LuaRouter
|
||||||
staticRouter *routers.StaticRouter
|
staticHandler fasthttp.RequestHandler
|
||||||
luaRunner *runner.Runner
|
staticFS *fasthttp.FS
|
||||||
fasthttpServer *fasthttp.Server
|
luaRunner *runner.Runner
|
||||||
loggingEnabled bool
|
fasthttpServer *fasthttp.Server
|
||||||
debugMode bool
|
loggingEnabled bool
|
||||||
config *config.Config
|
debugMode bool
|
||||||
sessionManager *sessions.SessionManager
|
config *config.Config
|
||||||
errorConfig utils.ErrorPageConfig
|
sessionManager *sessions.SessionManager
|
||||||
ctxPool sync.Pool
|
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,
|
runner *runner.Runner, loggingEnabled bool, debugMode bool,
|
||||||
overrideDir string, config *config.Config) *Server {
|
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{
|
s := &Server{
|
||||||
luaRouter: luaRouter,
|
luaRouter: luaRouter,
|
||||||
staticRouter: staticRouter,
|
luaRunner: runner,
|
||||||
luaRunner: runner,
|
loggingEnabled: loggingEnabled,
|
||||||
loggingEnabled: loggingEnabled,
|
debugMode: debugMode,
|
||||||
debugMode: debugMode,
|
config: config,
|
||||||
config: config,
|
sessionManager: sessions.GlobalSessionManager,
|
||||||
sessionManager: sessions.GlobalSessionManager,
|
staticDir: staticDir,
|
||||||
|
staticPrefix: staticPrefix,
|
||||||
|
staticPrefixBytes: []byte(staticPrefix),
|
||||||
errorConfig: utils.ErrorPageConfig{
|
errorConfig: utils.ErrorPageConfig{
|
||||||
OverrideDir: overrideDir,
|
OverrideDir: overrideDir,
|
||||||
DebugMode: debugMode,
|
DebugMode: debugMode,
|
||||||
},
|
},
|
||||||
ctxPool: sync.Pool{
|
ctxPool: sync.Pool{
|
||||||
New: func() any {
|
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{
|
s.fasthttpServer = &fasthttp.Server{
|
||||||
Handler: s.handleRequest,
|
Handler: s.handleRequest,
|
||||||
Name: "Moonshark/" + metadata.Version,
|
Name: "Moonshark/" + metadata.Version,
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
WriteTimeout: 30 * time.Second,
|
WriteTimeout: 30 * time.Second,
|
||||||
MaxRequestBodySize: 16 << 20,
|
IdleTimeout: 120 * time.Second,
|
||||||
TCPKeepalive: true,
|
MaxRequestBodySize: 16 << 20,
|
||||||
TCPKeepalivePeriod: 60 * time.Second,
|
TCPKeepalive: true,
|
||||||
ReduceMemoryUsage: true,
|
TCPKeepalivePeriod: 60 * time.Second,
|
||||||
DisablePreParseMultipartForm: true,
|
ReduceMemoryUsage: true,
|
||||||
|
DisablePreParseMultipartForm: true,
|
||||||
|
DisableHeaderNamesNormalizing: true,
|
||||||
|
NoDefaultServerHeader: true,
|
||||||
|
StreamRequestBody: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
@ -79,56 +141,48 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
|||||||
|
|
||||||
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
|
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
method := string(ctx.Method())
|
methodBytes := ctx.Method()
|
||||||
path := string(ctx.Path())
|
pathBytes := ctx.Path()
|
||||||
|
|
||||||
if s.debugMode && path == "/debug/stats" {
|
// Fast path for debug stats
|
||||||
|
if s.debugMode && bytes.Equal(pathBytes, debugPath) {
|
||||||
s.handleDebugStats(ctx)
|
s.handleDebugStats(ctx)
|
||||||
if s.loggingEnabled {
|
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
|
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{}
|
// Lua route lookup - only allocate params if found
|
||||||
bytecode, scriptPath, routeErr, found := s.luaRouter.GetRouteInfo(method, path, params)
|
bytecode, scriptPath, routeErr, params, found := s.luaRouter.GetRouteInfo(methodBytes, pathBytes)
|
||||||
|
|
||||||
if found {
|
if found {
|
||||||
if len(bytecode) == 0 || routeErr != nil {
|
if len(bytecode) == 0 || routeErr != nil {
|
||||||
errorMsg := "Route exists but failed to compile. Check server logs for details."
|
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, routeErr)
|
||||||
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)))
|
|
||||||
} else {
|
} else {
|
||||||
logger.Debug("Found Lua route match for %s %s with %d params", method, path, params.Count)
|
s.handleLuaRoute(ctx, bytecode, scriptPath, params, methodBytes, pathBytes)
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.SetContentType("text/html; charset=utf-8")
|
s.send404(ctx, pathBytes)
|
||||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
|
||||||
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, path)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.loggingEnabled {
|
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)
|
luaCtx := runner.NewHTTPContext(ctx)
|
||||||
defer luaCtx.Release()
|
defer luaCtx.Release()
|
||||||
|
|
||||||
@ -142,28 +196,47 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
|||||||
|
|
||||||
session := s.sessionManager.GetSessionFromRequest(ctx)
|
session := s.sessionManager.GetSessionFromRequest(ctx)
|
||||||
sessionMap["id"] = session.ID
|
sessionMap["id"] = session.ID
|
||||||
sessionMap["data"] = session.GetAll()
|
|
||||||
|
|
||||||
luaCtx.Set("method", method)
|
// Only get session data if not empty
|
||||||
luaCtx.Set("path", path)
|
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("host", string(ctx.Host()))
|
||||||
luaCtx.Set("session", sessionMap)
|
luaCtx.Set("session", sessionMap)
|
||||||
|
|
||||||
if params.Count > 0 {
|
// Handle params
|
||||||
paramMap := make(map[string]any, params.Count)
|
if params != nil && params.Count > 0 {
|
||||||
|
paramMap := s.paramsPool.Get().(map[string]any)
|
||||||
for i := 0; i < params.Count; i++ {
|
for i := 0; i < params.Count; i++ {
|
||||||
paramMap[params.Keys[i]] = params.Values[i]
|
paramMap[params.Keys[i]] = params.Values[i]
|
||||||
}
|
}
|
||||||
luaCtx.Set("params", paramMap)
|
luaCtx.Set("params", paramMap)
|
||||||
|
defer func() {
|
||||||
|
for k := range paramMap {
|
||||||
|
delete(paramMap, k)
|
||||||
|
}
|
||||||
|
s.paramsPool.Put(paramMap)
|
||||||
|
}()
|
||||||
} else {
|
} else {
|
||||||
luaCtx.Set("params", emptyMap)
|
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 {
|
if formData, err := ParseForm(ctx); err == nil {
|
||||||
luaCtx.Set("form", formData)
|
luaCtx.Set("form", formData)
|
||||||
} else {
|
} else {
|
||||||
logger.Warning("Error parsing form: %v", err)
|
if s.debugMode {
|
||||||
|
logger.Warning("Error parsing form: %v", err)
|
||||||
|
}
|
||||||
luaCtx.Set("form", emptyMap)
|
luaCtx.Set("form", emptyMap)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -172,23 +245,24 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
|||||||
|
|
||||||
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
|
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Error executing Lua route: %v", err)
|
logger.Error("Lua execution error: %v", err)
|
||||||
ctx.SetContentType("text/html; charset=utf-8")
|
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, err)
|
||||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
|
||||||
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, path, err.Error())))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
|
// Handle session updates
|
||||||
session.Clear()
|
if len(response.SessionData) > 0 {
|
||||||
delete(response.SessionData, "__clear_all")
|
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
|
||||||
}
|
session.Clear()
|
||||||
|
delete(response.SessionData, "__clear_all")
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range response.SessionData {
|
for k, v := range response.SessionData {
|
||||||
if v == "__SESSION_DELETE_MARKER__" {
|
if v == "__SESSION_DELETE_MARKER__" {
|
||||||
session.Delete(k)
|
session.Delete(k)
|
||||||
} else {
|
} else {
|
||||||
session.Set(k, v)
|
session.Set(k, v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +271,33 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
|||||||
runner.ReleaseResponse(response)
|
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) {
|
func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
|
||||||
stats := utils.CollectSystemStats(s.config)
|
stats := utils.CollectSystemStats(s.config)
|
||||||
routeCount, bytecodeBytes := s.luaRouter.GetRouteStats()
|
routeCount, bytecodeBytes := s.luaRouter.GetRouteStats()
|
||||||
@ -209,3 +310,24 @@ func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
|
|||||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||||
ctx.SetBody([]byte(utils.DebugStatsPage(stats)))
|
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"
|
"time"
|
||||||
|
|
||||||
"Moonshark/http"
|
"Moonshark/http"
|
||||||
"Moonshark/routers"
|
"Moonshark/router"
|
||||||
"Moonshark/runner"
|
"Moonshark/runner"
|
||||||
"Moonshark/sessions"
|
"Moonshark/sessions"
|
||||||
"Moonshark/utils/color"
|
"Moonshark/utils/color"
|
||||||
@ -26,8 +26,7 @@ import (
|
|||||||
// Moonshark represents the server and all its dependencies
|
// Moonshark represents the server and all its dependencies
|
||||||
type Moonshark struct {
|
type Moonshark struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
LuaRouter *routers.LuaRouter
|
LuaRouter *router.LuaRouter
|
||||||
StaticRouter *routers.StaticRouter
|
|
||||||
LuaRunner *runner.Runner
|
LuaRunner *runner.Runner
|
||||||
HTTPServer *http.Server
|
HTTPServer *http.Server
|
||||||
cleanupFuncs []func() error
|
cleanupFuncs []func() error
|
||||||
@ -143,7 +142,7 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) {
|
|||||||
cfg.Server.Debug = true
|
cfg.Server.Debug = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := initRouters(moonshark); err != nil {
|
if err := initLuaRouter(moonshark); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -155,9 +154,18 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) {
|
|||||||
logger.Warning("Watcher setup failed: %v", err)
|
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.HTTPServer = http.New(
|
||||||
moonshark.LuaRouter,
|
moonshark.LuaRouter,
|
||||||
moonshark.StaticRouter,
|
staticDir,
|
||||||
moonshark.LuaRunner,
|
moonshark.LuaRunner,
|
||||||
cfg.Server.HTTPLogging,
|
cfg.Server.HTTPLogging,
|
||||||
cfg.Server.Debug,
|
cfg.Server.Debug,
|
||||||
@ -165,6 +173,13 @@ func initServerMode(cfg *config.Config, debug bool) (*Moonshark, error) {
|
|||||||
cfg,
|
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
|
return moonshark, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,15 +264,15 @@ func dirExists(path string) bool {
|
|||||||
return info.IsDir()
|
return info.IsDir()
|
||||||
}
|
}
|
||||||
|
|
||||||
func initRouters(s *Moonshark) error {
|
func initLuaRouter(s *Moonshark) error {
|
||||||
if !dirExists(s.Config.Dirs.Routes) {
|
if !dirExists(s.Config.Dirs.Routes) {
|
||||||
return fmt.Errorf("routes directory doesn't exist: %s", s.Config.Dirs.Routes)
|
return fmt.Errorf("routes directory doesn't exist: %s", s.Config.Dirs.Routes)
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
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 err != nil {
|
||||||
if errors.Is(err, routers.ErrRoutesCompilationErrors) {
|
if errors.Is(err, router.ErrRoutesCompilationErrors) {
|
||||||
// Non-fatal, some routes failed
|
// Non-fatal, some routes failed
|
||||||
logger.Warning("Some routes failed to compile")
|
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))
|
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
|
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"
|
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
|
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 {
|
type Config struct {
|
||||||
// Server settings
|
// Server settings
|
||||||
Server struct {
|
Server struct {
|
||||||
Port int
|
Port int
|
||||||
Debug bool
|
Debug bool
|
||||||
HTTPLogging bool
|
HTTPLogging bool
|
||||||
|
StaticPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Runner settings
|
// Runner settings
|
||||||
@ -49,6 +50,7 @@ func New() *Config {
|
|||||||
config.Server.Port = 3117
|
config.Server.Port = 3117
|
||||||
config.Server.Debug = false
|
config.Server.Debug = false
|
||||||
config.Server.HTTPLogging = false
|
config.Server.HTTPLogging = false
|
||||||
|
config.Server.StaticPrefix = "static/"
|
||||||
|
|
||||||
// Runner defaults
|
// Runner defaults
|
||||||
config.Runner.PoolSize = runtime.GOMAXPROCS(0)
|
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 {
|
if v, ok := serverTable["http_logging"].(bool); ok {
|
||||||
config.Server.HTTPLogging = v
|
config.Server.HTTPLogging = v
|
||||||
}
|
}
|
||||||
|
if v, ok := serverTable["static_prefix"].(string); ok {
|
||||||
|
config.Server.StaticPrefix = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply runner settings
|
// Apply runner settings
|
||||||
|
@ -5,7 +5,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"Moonshark/routers"
|
"Moonshark/router"
|
||||||
"Moonshark/runner"
|
"Moonshark/runner"
|
||||||
"Moonshark/utils/color"
|
"Moonshark/utils/color"
|
||||||
"Moonshark/utils/logger"
|
"Moonshark/utils/logger"
|
||||||
@ -34,7 +34,7 @@ func ShutdownWatcherManager() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WatchLuaRouter sets up a watcher for a LuaRouter's routes directory
|
// 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()
|
manager := GetWatcherManager()
|
||||||
|
|
||||||
config := DirectoryWatcherConfig{
|
config := DirectoryWatcherConfig{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user