560 Commits

Author SHA1 Message Date
xfy
6967957299 feat(config): add ${ENV_VAR} interpolation in YAML configuration
Enable environment variable substitution in configuration files using
${VAR} syntax. Supports 12-factor app deployment patterns without
hardcoding secrets or environment-specific values.

Syntax:
- Only ${VAR} with curly braces (avoids conflict with $variable system)
- Missing variables preserved as-is (${MISSING} stays unchanged)
- Multiple variables per line supported
- Adjacent variables ${A}${B} handled correctly

Integration:
- Applied in config.Load() after os.ReadFile, before yaml.Unmarshal
- Applied in processIncludes() for each included file
- 12 unit tests covering single/multiple/missing/empty variables
2026-06-11 23:41:52 +08:00
xfy
f605ef3b44 feat(server): add /healthz and /readyz endpoints for Kubernetes probes
Add lightweight health check endpoints for container orchestration
(Kubernetes liveness/readiness probes, load balancer health checks).

New config under monitoring:
- healthz.enabled (default true), healthz.path (default /healthz)
- readyz.enabled (default true), readyz.path (default /readyz)

/healthz (liveness):
- Always returns 200 {"status":"ok"} if process is alive
- No dependency checks, minimal overhead

/readyz (readiness):
- Returns 200 {"status":"ready"} when server is running
- Returns 503 {"status":"not ready","reasons":[...]} when not ready
- Static-only servers (no proxies) always return 200

Registration:
- Registered alongside status/pprof endpoints
- Available in single mode (LocationEngine) and multi-server mode (Router)
- No IP allowlist required (K8s probes come from localhost)
- 6 unit tests covering all response scenarios
2026-06-11 23:41:45 +08:00
xfy
8224ae7ff3 feat(middleware/cors): add CORS middleware with server-level configuration
Implement Cross-Origin Resource Sharing (CORS) middleware following the
middleware.Middleware interface pattern.

New config under security.cors:
- enabled: toggle CORS handling (default false)
- allowed_origins: exact origin list or ["*"] wildcard
- allowed_methods: allowed HTTP methods for preflight
- allowed_headers: allowed request headers for preflight
- expose_headers: headers visible to frontend JS
- allow_credentials: send cookies (incompatible with wildcard origin)
- max_age: preflight cache duration in seconds

Validation:
- origins+credentials mutual exclusion per CORS spec
- max_age non-negative check

Integration:
- Registered after SecurityHeaders, before ErrorIntercept in middleware chain
- Preflight (OPTIONS) returns 204 with CORS headers, skips handler
- Actual requests add CORS headers after handler execution
- Non-matching origins pass through without CORS headers
- 16 unit tests covering all scenarios
2026-06-11 23:41:38 +08:00
xfy
6c538a1a56 feat(server,proxy): integrate Request-ID into middleware chain and proxy forwarding
- Register requestid.New() as first middleware in buildMiddlewareChain
  (before AccessLog) so the ID is available for logging
- Add SetRequestIDHeader() in proxy/headers.go to propagate X-Request-ID
  to upstream via proxy forwarding
- Call SetRequestIDHeader in header_modifier.go after SetForwardedHeaders
- Import requestid package in middleware_builder and proxy/headers
2026-06-11 23:41:30 +08:00
xfy
ebfa9cc7a8 feat(middleware/requestid): add request ID generation and propagation middleware
Implement Request-ID middleware that generates or propagates X-Request-ID
headers for distributed request tracing.

- Check incoming X-Request-ID header, reuse if present (trust downstream)
- Generate UUID v4 via google/uuid if no incoming ID
- Store ID in ctx.UserValue for variable system and access log access
- Set X-Request-ID response header for client-side tracing
- Add GetRequestID() helper for proxy header propagation
- Registered as first middleware (before AccessLog) so $request_id
  is available throughout the request lifecycle
- 8 unit tests covering generation, propagation, empty header, UUID format
2026-06-11 23:41:24 +08:00
xfy
f33117b940 fix(handler,http2,loadbalance,logging,resolver,ssl): fix high severity issues
- handler/static.go: add sync.RWMutex to StaticHandler; protect Handle
  with RLock and all setters with Lock to prevent data races
- http2/server.go: delete empty connection slice keys from pool map to
  prevent memory leak under high client churn
- loadbalance/slow_start.go: recreate stopCh in Start() to support
  Start-Stop-Start cycles; guard double-close in Stop()
- resolver/resolver.go: recreate stopCh in Start() to support restart
- logging/logging.go: save *os.File handles from getOutput so Close()
  actually closes log files; exclude os.Stdout/os.Stderr from closing
- ssl/session_tickets.go: protect started/rotateTimer access in
  scheduleRotation with mu; support Start-Stop-Start cycles
- ssl/ssl.go: cache parsed default certificate to avoid re-parsing on
  every TLS handshake for OCSP stapling
2026-06-11 17:03:17 +08:00
xfy
27e00b84a8 fix(proxy,handler,server,stream,ratelimit): fix resource leaks and functional bugs
- proxy/proxy.go: decrement connection count on dangerous path rejection
  (line 724) to prevent connection count leak
- handler/sendfile_linux.go: return *os.File from getSocketFile and let
  linuxSendfile close it, fixing EBADF from deferred close in getSocketFd
- proxy/websocket.go: return bufio.Reader from readWebSocketUpgradeResponse
  and wrap targetConn with bufferedConn to consume pre-buffered frame data,
  preventing first-frame loss
- server/pool.go: use non-blocking send after starting new worker to avoid
  deadlock when queue is full
- stream/stream.go: check stopCh on non-timeout UDP read errors to prevent
  infinite loop and shutdown deadlock
- middleware/ratelimit: replace select-based close guard with sync.Once in
  StopCleanup to prevent double-close panic
2026-06-11 16:35:10 +08:00
xfy
fe0dee4da3 fix(compression,ssl,server,lua): resolve data races and concurrency bugs
- compression: move sync.Pool.New initialization into constructors to
  eliminate lazy-init race in Get()
- ssl/ocsp: copy response fields under RLock before releasing, preventing
  race with concurrent writers in refreshAll
- server: change proxiesMu from sync.Mutex to sync.RWMutex; protect
  getProxyCacheStats and purge handlers with RLock to prevent races
  with proxy registration
- lua/api_timer: fix double-decrement race in Cancel vs executeTimer
  by using timer.Stop() result to determine who decrements active
- lua/api_socket_tcp: fix nil pointer race in ConnectAsync by checking
  currentOp under lock before Connect returns
2026-06-11 16:30:11 +08:00
xfy
e733273139 fix(server,app,proxy,resolver,middleware,lua): add nil guards and safe defaults
- server: reject Start() when config is nil to prevent panic
- app_common: guard empty Servers slice in initHTTP2/3 and logServerAddresses
- proxy/health: handle nil HealthCheckConfig with defaults
- resolver: handle nil ResolverConfig by returning noopResolver
- middleware/headers: skip UpdateConfig when cfg is nil
- middleware/sliding_window: enforce minimum window duration of 1s
- lua/api_log: map EMERG/ALERT/CRIT to Error() instead of Fatal()
  to prevent Lua scripts from killing the entire server process
2026-06-11 16:23:04 +08:00
xfy
818aa23739 fix(logging,mimeutil,variable): correct data corruption and behavior bugs
- logging: pre-allocate fresh slice for request field to avoid mutating
  fasthttp internal buffers via append into slices with excess capacity
- mimeutil: move defaultMIME fallback before cache insertion so unknown
  extensions are consistently cached as application/octet-stream
- builtin: use -0700 timezone format instead of hardcoded +0800; cache
  generated request_id in UserValue to prevent different IDs per expansion
- variable: initialize maps in fallback Context to prevent nil map panic
2026-06-11 16:22:55 +08:00
xfy
dea5e28f5f test(server): fix data race in monitoring endpoint tests
Replace concurrent polling of srv.GetListeners() with pre-created
net.Listener injected via srv.SetListeners(). The previous approach
raced with the Start() goroutine writing to s.listeners and calling
net.Listen().

Also remove unused waitForServerRunning call.
2026-06-11 15:23:46 +08:00
xfy
e8fbbf368c fix(config,server): merge defaults on Load and fix monitoring registration
Two related fixes that must land together:

1. config.Load() now starts from DefaultConfig() before unmarshaling
   YAML. This ensures missing top-level fields (Performance,
   Monitoring, Resolver) use their documented defaults instead of
   zero values. Most importantly, file_cache is no longer silently
   disabled when users omit the performance: section.

2. startSingleMode() now checks Monitoring.Status.Enabled instead of
   Path/Allow to decide whether to register the status endpoint.
   Without this change, fix #1 would have caused a regression where
   the status handler is registered even when monitoring is disabled,
   because DefaultConfig() sets Path and Allow defaults.

Also replace remaining log.Printf in status.go and lua/api_timer.go
with zerolog to follow project logging conventions.

Added tests:
- config/load_test.go: verifies defaults are applied, explicit values
  override defaults, and monitoring stays disabled by default.
- server/monitoring_registration_test.go: verifies /_status is only
  registered when enabled and remains reachable with static handler
  on path: /.
2026-06-11 15:08:57 +08:00
xfy
7cc76f0d5b chore(proxy): remove misplaced go:generate directive
The //go:generate go test -v ./... directive is not what
go:generate is intended for (code generation, not test execution).
Remove it to avoid confusion.
2026-06-11 15:02:19 +08:00
xfy
9824ad5b57 perf(netutil): cache RemoteAddr string formatting
Add netutil.FormatRemoteAddr() to avoid repeated net.TCPAddr.String()
allocations on the hot logging path.

- IPv4 addresses use a zero-allocation fast path (custom uint8->ASCII).
- IPv6 falls back to addr.String() with a 1024-entry LRU cache.
- Update logging.LogAccess and variable $remote_addr/$remote_port
  getters to use the shared helper.

Eliminates net.JoinHostPort and net.IP.String from top allocators
when access logging is active.
2026-06-11 14:43:12 +08:00
xfy
148f43fcb3 perf(handler): enable file info cache and fix index file cache lookup
- Router now enables FileInfoCache with 2s TTL for all static handlers
  to reduce redundant os.Stat calls.
- FileInfoCache supports negative caching: missing files are cached
  with a shorter TTL to avoid repeated stat on non-existent paths.
- Fix missing fileCache lookup for directory index files (index.html).
  Previously handleStandard/handleTryFiles skipped fileCache when
  serving index files, causing os.ReadFile on every request even
  with file_cache configured.
- Extract tryServeFromFileCache() helper to unify cache hit logic
  across file and index-file serving paths.

Verified with wrk 200 conn / 20s on static /index.html:
- Throughput: 140k -> 242k req/sec (+73%)
- alloc_space: 2.6 GB -> 4.6 MB (-99.8%)
2026-06-11 14:43:04 +08:00
xfy
047e033af5 feat(accesslog): add deterministic sampling with sample_rate config
Add logging.access.sample_rate config (0.0-1.0) for deterministic
request sampling. 5xx errors are always logged; 2xx/3xx/4xx follow
the configured rate. Uses atomic.Uint64 counter for lock-free,
zero-allocation sampling decisions.

Includes test updates to verify:
- sample_rate=1.0 logs all requests
- sample_rate=0.0 logs only 5xx
- 5xx are always logged regardless of rate
2026-06-11 14:42:55 +08:00
xfy
1128eb644f perf(static): enable FileInfoCache by default with negative caching
Production static file serving now uses FileInfoCache by default
with a 2-second TTL in router.go, dramatically reducing os.Stat
syscalls for missing files and repeated paths.

Changes:
- Add negative cache support to FileInfoCache (caches 'not found' results)
- Introduce statWithCache() helper in StaticHandler for uniform caching
- Make FileInfoCache TTL configurable via SetTTL()
- Default cacheTTL=0 disables caching in NewStaticHandler (tests compat)
- router.go enables fileInfoCache with 2s TTL for all static handlers

Benchmark (repeated 404s):
  No cache:    ~2651 ns/op, 2225 B/op, 15 allocs/op
  With cache:  ~1505 ns/op, 1905 B/op, 12 allocs/op
  Improvement: -43% latency, -14% allocations

This addresses the dominant allocation source in v0.4.0 profile
(os.statNolog at 74.95% of allocations).
2026-06-11 14:05:56 +08:00
xfy
445401c40f perf(accesslog): add sample_rate for access log to reduce CPU and allocations
Add configurable access log sampling via :
- 0.0-1.0 range; defaults to 1.0 (record all) for backward compatibility
- Uses lock-free atomic counter for deterministic sampling
- Non-2xx responses always logged regardless of sample rate

Benchmark results (combined format, /dev/null):
  Full logging:    ~2245 ns/op, 1987 B/op, 17 allocs/op
  10% sampling:    ~1593 ns/op, 1633 B/op,  6 allocs/op
  Improvement:     -29% latency, -65% allocations/op

This addresses the top application-layer CPU hotspot identified
in the v0.4.0 profile (LogAccess at 16.36% cumulative CPU).
2026-06-11 13:53:41 +08:00
xfy
58e095a35b feat(pprof): add /debug/pprof/allocs endpoint for allocation profiling
- Add writeAllocsProfile() helper in pprof_impl.go
- Register /allocs route in PprofHandler.ServeHTTP
- Add handleAllocs() method with proper streaming response
- Update index page to list the new allocs profile link

This aligns lolly's pprof endpoints with net/http/pprof and enables
allocation hotspot analysis during performance benchmarking.
2026-06-11 13:47:47 +08:00
xfy
66ea93e3c1 fix(stream): reset stopCh after Stop for restartability 2026-06-10 13:48:07 +08:00
xfy
7204432ca0 fix(stream): correct upstream selection and add graceful shutdown
- Fix handleConnection to use addr parameter for direct upstream map
  lookup instead of always selecting the first upstream
- Add Server.Stop() for graceful shutdown with listener closing, UDP
  server cleanup, health checker termination, and goroutine joining
- Add shutdownStream() to App and call it in SIGTERM/SIGQUIT/SIGUSR2
  signal handlers to prevent goroutine and port leaks on shutdown
2026-06-10 13:45:35 +08:00
xfy
f12ffd180f chore: release v0.4.0
- Update CHANGELOG.md for v0.4.0
- Update Makefile FALLBACK_VERSION to 0.4.0
- Fix lint warnings (godoc comments, goconst)
- Clean up code formatting
2026-06-09 15:59:36 +08:00
xfy
503daf65d3 perf(loadbalance): add benchmarks for Least Time and Sticky
- Benchmark Select and Record operations
- Concurrent benchmark for realistic load testing
- Baseline performance:
  - LeastTime.Select: ~33ns/op, 0 allocs
  - LeastTime.Record: ~5.6ns/op, 0 allocs
  - StickySession.Select: ~205ns/op (with cookie lookup)
2026-06-08 18:21:03 +08:00
xfy
ef871f1d39 test(loadbalance): add integration tests for Least Time and Sticky
- Verify Least Time picks faster target consistently
- Verify Sticky fallback when target becomes unhealthy
- Test cookie encoding and session persistence
2026-06-08 18:19:20 +08:00
xfy
e5885ce888 fix(proxy): correct response time recording for Least Time
- Record headerTime when header is received
- Record lastByteTime when response is complete
- Use correct timing calculations (headerReceived/connectEnd/responseEnd)
2026-06-08 18:17:08 +08:00
xfy
72f189bba8 feat(proxy): integrate Least Time and Sticky balancers
- Add least_time and sticky to createBalancerByName
- Implement response time recording for Least Time
- Support StickySession in target selector with request context
- StickySession auto-starts when created
2026-06-08 18:11:47 +08:00
xfy
3b6b70a491 fix(config): validate least_time default_time is not negative 2026-06-08 18:03:52 +08:00
xfy
cb1f86298e fix: add missing test coverage for Task 4 config integration
- Add validation tests for least_time and sticky configs
- Add algorithm tests for least_time and sticky
- Add SameSite validation in validateProxy
2026-06-08 18:01:21 +08:00
xfy
88a2c1fc1b feat(config): add Least Time and Sticky configuration support
- Add least_time and sticky to valid algorithms list
- Add LeastTimeConfig and StickyConfig structures
- Update default config generation with new options
- Add configuration validation for new fields
2026-06-08 17:57:06 +08:00
xfy
a73da4e14a fix(sticky): recreate stopCh on Start to support restart 2026-06-08 17:52:20 +08:00
xfy
0a5443f6cf fix(sticky): guard against double Stop, nil fallback, and multiple Start calls
- Add sync.Once to prevent double close of stopCh in Stop()
- Add nil fallback guard in NewStickySession (defaults to RoundRobin)
- Add atomic.Bool to make Start() idempotent
- Add tests for double Stop() and nil fallback scenarios
2026-06-08 17:47:37 +08:00
xfy
360fd0da9d fix(sticky): check cookie expiration in Select method
- Fix Select to check if cookie is expired before routing
- Add TestStickySession_ExpiredCookie test
- Expired cookies now trigger fallback + new cookie set
2026-06-08 17:40:54 +08:00
xfy
66752a47f0 fix(sticky): fix cookie format, shard keying, and tests
- Encode cookie as base64(target_url + | + timestamp) per spec
- Use cookie value (not targetURL) for shard key and session map keys
- Add missing sticky.Start() calls in tests
- Fix time precision in cookie encode/decode tests
2026-06-08 17:36:41 +08:00
xfy
f69a11ea05 feat(loadbalance): implement Session Sticky balancer
- Add 256-shard lock map for concurrent session routing
- Cookie-based session persistence with base64 encoding
- TTL expiration with background cleanup goroutine
- Support Secure, HttpOnly, SameSite cookie attributes
- Fallback to configured balancer when session target unavailable
2026-06-08 17:30:06 +08:00
xfy
fa95b2a76e feat(loadbalance): implement Least Time balancer
- Add atomic EWMA Stats field to Target
- Implement LeastTime balancer with header_time and last_byte metrics
- Support Select and SelectExcluding with zero-lock design
- Add ResponseTimeRecorder interface for proxy integration
2026-06-08 17:21:20 +08:00
xfy
c6bb75cffe feat(loadbalance): add atomic EWMA statistics core
- Zero-lock atomic EWMA implementation using fixed-point arithmetic
- Supports header_time and last_byte_time tracking
- Concurrent-safe with CAS retry loop
2026-06-08 17:13:08 +08:00
xfy
85ae7747b8 fix(integration): remove calls to removed proxy.Start/Stop methods 2026-06-05 14:24:34 +08:00
xfy
93c0c151d0 fix(lua): wait for SchedulerLoop exit before closing LState; lock cleanupResources 2026-06-05 13:48:04 +08:00
xfy
4789265ca8 fix: add synchronization for concurrent access in server/app/http3/stream 2026-06-05 12:31:41 +08:00
xfy
5e3196c37e fix: resolve race conditions in handler sendfile and lua cosocket tests 2026-06-05 12:31:39 +08:00
xfy
f73a761632 fix(server): protect accessLogMiddleware and accessControl from concurrent writes 2026-06-05 11:49:19 +08:00
xfy
76257a7859 fix(lua): add schedulerMu to protect scheduler LState and callback queue 2026-06-05 11:38:52 +08:00
xfy
2be04f3fb9 fix(lua): add mutex protection for TCPSocket.currentOp in async methods 2026-06-05 11:35:20 +08:00
xfy
31faf77fcc style: add doc comments for exported hash and utils functions
Fix revive lint warnings for FNV64a, FNV64aBytes, BytesContainsFold.
2026-06-04 11:17:08 +08:00
xfy
2be6b67d0b fix(server): release MatchResult back to pool after use
Add matcher.ReleaseMatchResult(result) in the base handler to prevent
sync.Pool object leak. Every Match() call acquires from pool but the
caller never returned objects, causing unbounded pool growth.
2026-06-04 11:14:32 +08:00
xfy
10f16bfda9 test(ssl): update extractPEMBlock tests for DER output
Verify returned bytes are parseable by x509.ParseCertificate instead
of checking raw PEM text markers.
2026-06-04 11:14:23 +08:00
xfy
434ac0b114 fix(ssl): use encoding/pem for DER extraction in extractPEMBlock
Replace manual PEM text scanning with pem.Decode(). Returns proper
DER-encoded bytes instead of raw PEM text, fixing potential TLS
handshake failures with certificate chains.

Remove unused findMarker and matchMarker helpers.
2026-06-04 11:14:13 +08:00
xfy
197d0d2344 perf(security): reduce GeoIP lookups and deduplicate trusted proxy check
- Check(): single GeoIP LookupCountry call, result reused for both
  deny and allow checks. Removed goto label for structured flow.
- getClientIP(): single trusted proxy CIDR scan gates both
  X-Forwarded-For and X-Real-IP processing.
2026-06-04 11:09:29 +08:00
xfy
e535b9062c perf(gzip_static): pre-build extension set, use BytesContainsFold
- Pre-build extSet map for O(1) extension lookup instead of linear scan
- Replace bytes.ToLower allocation in supportsEncoding with
  utils.BytesContainsFold for case-insensitive encoding detection
2026-06-04 11:09:20 +08:00
xfy
e5fa9fe9de perf(compression): pre-compute MIME type byte slices for isCompressible
Add typesBytes and typesWildcardPrefix fields to Middleware, built once
at construction. isCompressible now uses pre-converted byte slices
instead of allocating []byte(t) per comparison per request.
2026-06-04 11:09:08 +08:00