Documentation
¶
Overview ¶
Package compiler provides route compilation for the Rivaas router.
The compiler package implements a route matching system through pre-compiled route tables. It improves routing by:
- Pre-computing route structure information
- Using bloom filters for negative lookups
- Building first-segment indexes for route narrowing
- Enabling single-pass validation and parameter extraction
Architecture ¶
The compiler uses a three-tier strategy for route matching:
- Static routes: Hash table lookup
- Dynamic routes: Pre-compiled patterns with segment-based matching
- Complex routes: Tree fallback
This hybrid approach handles the most common cases while maintaining correctness for complex routing scenarios.
Route Compilation ¶
Routes are compiled through the CompileRoute function, which:
- Analyzes route structure (static/dynamic/wildcard segments)
- Counts segments for length validation
- Stores parameter positions for extraction
- Attaches constraint patterns for validation
- Computes specificity score for sorting
Example:
route := compiler.CompileRoute(
"GET",
"/api/users/:id/posts/:pid",
handlers,
[]compiler.RouteConstraint{
{Param: "id", Pattern: regexp.MustCompile(`^\d+$`)},
{Param: "pid", Pattern: regexp.MustCompile(`^\d+$`)},
},
)
Bloom Filter ¶
The bloom filter provides negative lookups for static routes:
- Membership tests
- Zero false negatives (if not in bloom filter, definitely not in routes)
- Rare false positives (< 1% with proper sizing)
- Checks
The bloom filter eliminates map lookups for non-existent routes.
First-Segment Index ¶
The first-segment index groups dynamic routes by their first static segment:
/users/:id -> index["users"] /posts/:pid -> index["posts"] /api/:resource -> index["api"]
This approach:
- Reduces candidate routes for typical workloads
- Enables route narrowing before pattern matching
- Lazy-initializes on first dynamic route match
- Only indexes ASCII segments (non-ASCII routes skip indexing)
Lookup Details ¶
Implementation:
- Static routes: Hash table lookup
- Dynamic routes: Segment-based matching
- Bloom filter size depends on route count
Typical behavior:
- Static lookup: Hash table lookup
- Dynamic match: Segment-based matching
- Constrained match: Pattern validation with matching
Design Decisions ¶
## ASCII-Only First-Segment Index
The first-segment index only handles ASCII characters for simplicity:
- Avoids UTF-8 decoding in hot path
- Simplifies case-insensitive comparisons (single tolower operation)
- Non-ASCII routes fall back to linear scan (acceptable trade-off)
- Most web APIs use ASCII paths anyway
## Import Cycle Prevention
The compiler package is designed to avoid import cycles with the router package:
- Defines its own HandlerFunc type (function, not interface)
- Uses []HandlerFunc instead of HandlerChain
- Defines minimal RouteConstraint type
- No dependencies on router internals
This allows the router package to import compiler without circular dependencies.
## Storage
Compiled routes store minimal metadata:
- Fixed-size arrays for parameter positions (max 8 params)
- String slices are shared, not copied
- Constraints stored as slice of structs (not pointers)
- Handler functions shared across router and compiler
Thread Safety ¶
All operations are thread-safe:
- RouteCompiler uses RWMutex for concurrent access
- Bloom filter is read-only after initialization
- First-segment index uses sync.Once for lazy initialization
- Route compilation is a pure function
Concurrent reads (route matching) do not block each other. Route additions (compilation) are synchronized with a mutex.
Usage Example ¶
Basic usage in the router:
// Initialize compiler
rc := compiler.NewRouteCompiler(1000, 3) // 1000 routes, 3 hash funcs
// Register routes
r.GET("/users/:id", handler)
r.GET("/posts/:pid", handler)
// Compile routes (called by router.Warmup)
rc.CompileAllRoutes()
// Route matching (called by router.ServeHTTP)
if route := rc.LookupStatic("GET", "/users"); route != nil {
// Found static route
}
params := make(map[string]string)
if route := rc.MatchDynamic("GET", "/users/123", params); route != nil {
// Found dynamic route, params["id"] = "123"
}
See Also ¶
- bloom.go: Bloom filter implementation for negative lookups
- static.go: Static route compilation and lookup
- dynamic.go: Dynamic route compilation and matching
- compiler.go: Main RouteCompiler and route compilation logic
Index ¶
- type BloomFilter
- type CompiledRoute
- type ContextParamWriter
- type HandlerFunc
- type RouteCompiler
- func (rc *RouteCompiler) AddRoute(route *CompiledRoute)
- func (rc *RouteCompiler) Freeze()
- func (rc *RouteCompiler) HasStatic() bool
- func (rc *RouteCompiler) HasStaticRoutes() bool
- func (rc *RouteCompiler) IsFrozen() bool
- func (rc *RouteCompiler) LookupStatic(method, path string) *CompiledRoute
- func (rc *RouteCompiler) MatchDynamic(method, path string, ctx ContextParamWriter) *CompiledRoute
- func (rc *RouteCompiler) RemoveRoute(method, pattern string)
- type RouteConstraint
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BloomFilter ¶
type BloomFilter struct {
// contains filtered or unexported fields
}
BloomFilter provides a simple bloom filter implementation for negative lookups. A bloom filter is a probabilistic data structure that can tell you: - "Definitely NOT in the set" (100% accurate) - "Possibly in the set" (may have false positives)
Use case in routing: Filter out paths that definitely don't exist before checking the route map.
How it works: 1. Hash the input with multiple hash functions (using different seeds) 2. Set bits at the hash positions when adding elements 3. Check if all bits are set when testing membership 4. If any bit is unset → element definitely not in set (true negative) 5. If all bits are set → element might be in set (check actual map)
Uses multiple hash functions (typically 3) Uses bit array for compact storage
Implementation using FNV-1a hash with different seeds
func NewBloomFilter ¶
func NewBloomFilter(size uint64, numHashFuncs int) *BloomFilter
NewBloomFilter creates a new bloom filter with the specified size and hash functions. Uses FNV-1a hash with different seeds.
Example ¶
ExampleNewBloomFilter demonstrates creating and using a bloom filter.
package main
import (
"fmt"
"rivaas.dev/router/compiler"
)
func main() {
// Create a bloom filter with size 1000 and 3 hash functions
bf := compiler.NewBloomFilter(1000, 3)
// Add items to the filter
bf.Add([]byte("GET/users"))
bf.Add([]byte("GET/posts"))
// Test membership
if bf.Test([]byte("GET/users")) {
fmt.Println("GET/users: possibly in set")
}
if !bf.Test([]byte("GET/comments")) {
fmt.Println("GET/comments: definitely not in set")
}
}
Output: GET/users: possibly in set GET/comments: definitely not in set
func (*BloomFilter) Add ¶
func (bf *BloomFilter) Add(data []byte)
Add adds an element to the bloom filter
func (*BloomFilter) Test ¶
func (bf *BloomFilter) Test(data []byte) bool
Test checks if an element might be in the bloom filter
Uses early-exit loop for handling of miss cases (common in routing). Bloom filters are most valuable when they can reject non-existent routes, so early exit on first failed bit check is important.
func (*BloomFilter) TestWithPrecomputedHash ¶ added in v0.4.0
func (bf *BloomFilter) TestWithPrecomputedHash(baseHash uint64) bool
TestWithPrecomputedHash checks if an element might be in the bloom filter using a pre-computed FNV-1a hash. This avoids recomputing the hash when the caller has already computed it.
Uses early-exit loop for handling of miss cases (common in routing).
type CompiledRoute ¶
type CompiledRoute struct {
// contains filtered or unexported fields
}
CompiledRoute represents a pre-compiled route with metadata for matching. It pre-computes route structure information during registration, including segment positions, parameter names, and constraint patterns.
func CompileRoute ¶
func CompileRoute(method, pattern string, handlers []HandlerFunc, constraints []RouteConstraint) *CompiledRoute
CompileRoute compiles a route pattern into a compiled route for matching. It pre-computes route structure information during registration, including segment positions, parameter names, and constraint patterns.
Example ¶
ExampleCompileRoute demonstrates compiling a simple static route.
package main
import (
"fmt"
"rivaas.dev/router/compiler"
)
func main() {
// Compile a static route (no parameters)
route := compiler.CompileRoute("GET", "/api/users", nil, nil)
fmt.Println("Pattern:", route.Pattern())
fmt.Println("Method:", route.Method())
}
Output: Pattern: /api/users Method: GET
Example (WithConstraints) ¶
ExampleCompileRoute_withConstraints demonstrates compiling a route with parameter constraints.
package main
import (
"fmt"
"regexp"
"rivaas.dev/router/compiler"
)
func main() {
// Define constraints for parameters
constraints := []compiler.RouteConstraint{
{Param: "id", Pattern: regexp.MustCompile(`^\d+$`)}, // id must be numeric
{Param: "pid", Pattern: regexp.MustCompile(`^\d+$`)}, // pid must be numeric
}
// Compile route with constraints
route := compiler.CompileRoute("GET", "/users/:id/posts/:pid", nil, constraints)
fmt.Println("Pattern:", route.Pattern())
fmt.Println("Method:", route.Method())
}
Output: Pattern: /users/:id/posts/:pid Method: GET
Example (WithParameters) ¶
ExampleCompileRoute_withParameters demonstrates compiling a route with parameters.
package main
import (
"fmt"
"rivaas.dev/router/compiler"
)
func main() {
// Compile a route with URL parameters
route := compiler.CompileRoute("GET", "/users/:id/posts/:pid", nil, nil)
fmt.Println("Pattern:", route.Pattern())
fmt.Println("Method:", route.Method())
}
Output: Pattern: /users/:id/posts/:pid Method: GET
func (*CompiledRoute) CachedHandlers ¶
func (r *CompiledRoute) CachedHandlers() unsafe.Pointer
CachedHandlers returns the cached converted handlers, or nil if not set. Returns unsafe.Pointer to []router.HandlerFunc.
func (*CompiledRoute) Handlers ¶
func (r *CompiledRoute) Handlers() []HandlerFunc
Handlers returns the handler chain for this route
func (*CompiledRoute) Method ¶
func (r *CompiledRoute) Method() string
Method returns the HTTP method for this route
func (*CompiledRoute) Pattern ¶
func (r *CompiledRoute) Pattern() string
Pattern returns the route pattern (e.g., "/users/:id")
func (*CompiledRoute) SetCachedHandlers ¶
func (r *CompiledRoute) SetCachedHandlers(handlers unsafe.Pointer)
SetCachedHandlers stores the converted handler slice. The handlers parameter should be a pointer to []router.HandlerFunc. This is called once during route compilation by the router.
type ContextParamWriter ¶
type ContextParamWriter interface {
SetParam(index int, key, value string)
SetParamMap(key, value string)
SetParamCount(count int32)
}
ContextParamWriter is an interface for writing route parameters to a context. This avoids import cycles by not importing router.Context directly.
type HandlerFunc ¶
type HandlerFunc any
HandlerFunc defines the handler function signature. This is a copy of router.HandlerFunc to avoid import cycles.
type RouteCompiler ¶
type RouteCompiler struct {
// contains filtered or unexported fields
}
RouteCompiler manages compiled routes for lookup. It organizes routes into static routes (exact path matches) and dynamic routes (routes with parameters). Routes are matched using the compiled metadata stored in CompiledRoute.
func NewRouteCompiler ¶
func NewRouteCompiler(bloomSize uint64, numHashFuncs int) *RouteCompiler
NewRouteCompiler creates a new route compiler
Example ¶
ExampleNewRouteCompiler demonstrates creating a new route compiler.
package main
import (
"fmt"
"rivaas.dev/router/compiler"
)
func main() {
// Create a route compiler with bloom filter size 1000 and 3 hash functions
rc := compiler.NewRouteCompiler(1000, 3)
// Add routes to the compiler
route := compiler.CompileRoute("GET", "/users", nil, nil)
rc.AddRoute(route)
fmt.Println("Route compiler created successfully")
}
Output: Route compiler created successfully
func (*RouteCompiler) AddRoute ¶
func (rc *RouteCompiler) AddRoute(route *CompiledRoute)
AddRoute adds a compiled route to the compiler
Example ¶
ExampleRouteCompiler_AddRoute demonstrates adding routes to a compiler.
package main
import (
"fmt"
"rivaas.dev/router/compiler"
)
func main() {
rc := compiler.NewRouteCompiler(1000, 3)
// Add a static route
staticRoute := compiler.CompileRoute("GET", "/health", nil, nil)
rc.AddRoute(staticRoute)
// Add a dynamic route
dynamicRoute := compiler.CompileRoute("GET", "/users/:id", nil, nil)
rc.AddRoute(dynamicRoute)
fmt.Println("Routes added successfully")
}
Output: Routes added successfully
func (*RouteCompiler) Freeze ¶ added in v0.4.0
func (rc *RouteCompiler) Freeze()
Freeze marks the route compiler as immutable. After calling Freeze, the mutex is bypassed in lookup operations since the data can no longer change. This eliminates lock overhead in the hot path.
Freeze also builds the first-segment index if there are enough routes. This should be called after all routes are registered.
func (*RouteCompiler) HasStatic ¶ added in v0.4.0
func (rc *RouteCompiler) HasStatic() bool
HasStatic returns true if any static routes are registered. This is a cheap check that avoids calling LookupStatic entirely.
func (*RouteCompiler) HasStaticRoutes ¶ added in v0.4.0
func (rc *RouteCompiler) HasStaticRoutes() bool
HasStaticRoutes returns true if there are any static routes registered. This can be used to skip LookupStatic entirely when there are no static routes. After Freeze(), this returns a cached value for maximum performance.
func (*RouteCompiler) IsFrozen ¶ added in v0.4.0
func (rc *RouteCompiler) IsFrozen() bool
IsFrozen returns true if the route compiler has been frozen.
func (*RouteCompiler) LookupStatic ¶
func (rc *RouteCompiler) LookupStatic(method, path string) *CompiledRoute
LookupStatic attempts to find a static route in the hash table. After Freeze() is called, this method bypasses the mutex for better performance.
Optimized to avoid allocations: - Computes FNV-1a hash inline without creating hash objects - Uses pre-computed hash for bloom filter test - Skips entirely if no static routes are registered
Example ¶
ExampleRouteCompiler_LookupStatic demonstrates looking up static routes.
package main
import (
"fmt"
"rivaas.dev/router/compiler"
)
func main() {
rc := compiler.NewRouteCompiler(1000, 3)
// Add static routes
route := compiler.CompileRoute("GET", "/health", nil, nil)
rc.AddRoute(route)
// Lookup the route
found := rc.LookupStatic("GET", "/health")
if found != nil {
fmt.Println("Found route:", found.Pattern())
}
// Lookup non-existent route
notFound := rc.LookupStatic("GET", "/nonexistent")
if notFound == nil {
fmt.Println("Route not found")
}
}
Output: Found route: /health Route not found
func (*RouteCompiler) MatchDynamic ¶
func (rc *RouteCompiler) MatchDynamic(method, path string, ctx ContextParamWriter) *CompiledRoute
MatchDynamic attempts to match path against dynamic routes. Uses first-segment index for filtering. After Freeze() is called, this method bypasses the mutex for better performance.
func (*RouteCompiler) RemoveRoute ¶
func (rc *RouteCompiler) RemoveRoute(method, pattern string)
RemoveRoute removes a route from the compiler (used when updating constraints)
type RouteConstraint ¶
RouteConstraint represents a compiled constraint for route parameters. This is a copy of router.RouteConstraint to avoid import cycles.