Documentation
¶
Overview ¶
Package luart is a reusable, thread-safe runtime for executing many Lua scripts (lua-pure) from many goroutines. It lazily loads script sources from an external SourceLoader, compiles once per key:version, pools preloaded LStates per script, evicts idle pools, caps total VMs by a memory budget, and hot-reloads on external notification (drop-and-reload).
Observability (Metrics, Logger), config loading (subpackage luartconfig: JSON/YAML/env), graceful shutdown (Shutdown), and developer profiling (TraceHook) are all opt-in and add no required dependency beyond lua-pure.
Since: 2026-06-07
Index ¶
- Variables
- func HashVersion(src string) string
- type Change
- type CompileCache
- type Config
- type Logger
- type MapLoader
- type Metrics
- type PoolStat
- type Runtime
- func (m *Runtime) Close()
- func (m *Runtime) CompileCount() int64
- func (m *Runtime) Notify(key, version, displayVersion string)
- func (m *Runtime) NotifyChanges(changes []Change)
- func (m *Runtime) PoolStats() []PoolStat
- func (m *Runtime) Run(ctx context.Context, key, entryFn string, args ...lua.Value) ([]lua.Value, error)
- func (m *Runtime) RunValues(ctx context.Context, key, entryFn string, args ...lua.Value) ([]any, error)
- func (m *Runtime) RunWith(ctx context.Context, key, entryFn string, ...) error
- func (m *Runtime) Shutdown(ctx context.Context) error
- func (m *Runtime) Stats() Stats
- type SourceLoader
- type Stats
- type TraceHook
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrClosed = errors.New("luart: runtime closed")
ErrClosed is returned when a closed Runtime is used.
var ErrInstructionLimit = errors.New("luart: instruction limit exceeded")
ErrInstructionLimit is returned by Run when a script exceeds Config.MaxInstructions (a runaway pure-Lua CPU cap, orthogonal to ExecTimeout). Use errors.Is(err, ErrInstructionLimit) to detect it.
Functions ¶
func HashVersion ¶
HashVersion returns the content hash (sha256 hex) of src — the recommended engine-facing version (any content change yields a different version). The external system usually provides it; this helper is for tests, demos, and self-verification. Since: 2026-06-07
Types ¶
type Change ¶
type Change struct {
Key string
Version string // engine version (content hash)
DisplayVersion string // human label (optional)
}
Change is one external change notification.
type CompileCache ¶
type CompileCache struct {
// contains filtered or unexported fields
}
CompileCache compiles sources into immutable *lua.Proto and caches them permanently.
func (*CompileCache) CompileCount ¶
func (c *CompileCache) CompileCount() int64
CompileCount returns the number of actual compiles (distinct key:version).
func (*CompileCache) GetOrCompile ¶
func (c *CompileCache) GetOrCompile(cacheKey, name, src string) (*lua.Proto, error)
GetOrCompile returns the compile result for cacheKey (the same key compiles exactly once; different keys compile in parallel). cacheKey is normally of the form "scriptKey:version".
type Config ¶
type Config struct {
// MaxStates, if > 0, is the global cap. If 0, it is derived from
// MemoryBudgetBytes divided by the measured per-state heap cost.
MaxStates int
MemoryBudgetBytes uint64
IdleTTL time.Duration // pools idle longer than this are evicted whole
JanitorInterval time.Duration // janitor sweep period
// ExecTimeout, if > 0, is a per-execution hard cap: each Run binds the
// running LState to a context with this deadline so a runaway (e.g. infinite
// loop) script is aborted. 0 disables it (no per-opcode context check, zero
// overhead). A cancelable ctx passed to Run is honored regardless. See Run.
// Since: 2026-06-07
ExecTimeout time.Duration
// MaxInstructions, if > 0, caps how many Lua bytecode instructions a single Run
// may execute before it is aborted with ErrInstructionLimit. It is the runaway
// pure-Lua CPU guard and is orthogonal to ExecTimeout: ExecTimeout is a
// wall-clock budget (covers blocking I/O in callbacks via the ctx passed to
// Run / L.Context()), while MaxInstructions bounds only opcode count — so a
// blocking but cooperative callback is not charged against it. It is enforced
// only at the engine's finalizer-poll gate, so the effective cap is rounded up
// to that granularity. Reset per Run (no carry-over across pooled reuse). 0
// disables it (zero overhead). Pure-Lua only — a blocking Go callback cannot be
// preempted by it (use ExecTimeout + a cancelable ctx for that).
// Since: 2026-06-28
MaxInstructions uint64
Libs []func(*lua.LState) // libraries to open on pooled States (defaults to defaultLibs)
// ExtraLibs are opened on each pooled State after Libs, so you can add custom
// libraries — register Go functions or module tables, or L.Preload lazy
// (require) modules — without restating the default set. Leaving Libs at its
// default and setting ExtraLibs is the safe "keep the sandbox, add mine" path.
// Each entry runs once per pooled State (in newState), on the goroutine that
// owns the State, so any shared Go state a lib closes over must be
// goroutine-safe. Opened after Libs — hence after defaultLibs'
// stripUnsafeLoaders — so re-adding load/loadfile/dofile here un-sandboxes the
// State. With IsolateGlobals, globals a lib defines are read-visible to each
// call via the shared-library fallback (a mutable shared table a lib exposes
// can be mutated in place across calls, so prefer immutable/functional libs).
// Since: 2026-07-05
ExtraLibs []func(*lua.LState)
// IsolateGlobals, when true, runs each call under a fresh per-call _ENV (Lua
// 5.4 environment sandbox) so global writes do not leak across Runs that reuse
// the same pooled State. Without it, the same pooled State carries a script's
// globals (e.g. `counter = counter + 1`) into the next caller, making results
// depend on which idle State is picked. Reads fall back to the shared
// libraries. The trade-off is a chunk re-run per call (cost proportional to the
// script's top-level definitions), so it is off by default (globals persist on
// the pooled State — the fastest path). Requires the base library (setmetatable)
// in Libs. See Run.
// Since: 2026-06-28
IsolateGlobals bool
// ConvertValue, when set, lets RunValues materialize non-data return values
// (function/userdata/thread) into Go values; it runs while the State still owns
// the value. If nil, RunValues returns an error for such values (the safe
// default — it never hands back a value tied to a reused State). Data values
// (nil/bool/number/string/table) are converted without it.
// Since: 2026-06-28
ConvertValue func(L *lua.LState, lv lua.Value) (any, error)
Metrics Metrics // monitoring sink (defaults to a no-op)
Logger Logger // structured logger (defaults to a no-op)
// Trace, if set, receives per-stage request timing for developer profiling.
// Leaving it nil has zero overhead (no timing is taken). See TraceHook.
Trace TraceHook
}
Config tunes Runtime behavior.
type Logger ¶
Logger receives structured log events as a message plus key/value pairs (slog-style). The zero-overhead default is noopLogger. Since: 2026-06-07
func NewSlogLogger ¶
NewSlogLogger adapts a *slog.Logger to the Logger interface. Pass it via Config.Logger to route luart events into slog. Since: 2026-06-07
type MapLoader ¶
type MapLoader struct {
// contains filtered or unexported fields
}
MapLoader is an in-memory SourceLoader for tests and demos. Set simulates an external change.
func (*MapLoader) Load ¶
Load returns the (source, version, displayVersion) for key, or an error if the key is unknown.
type Metrics ¶
type Metrics interface {
OnCompile(key string) // a source was loaded and compiled (new pool created)
OnBuild(key string) // a new LState was built (pool miss)
OnReuse(key string) // an idle LState was reused (pool hit)
OnEvict(key string) // an idle LState was LRU-evicted at the cap
OnDrop(key string) // a pool was dropped by a change notification
}
Metrics receives lifecycle events for monitoring. Implementations must be safe for concurrent use. The zero-overhead default is noopMetrics, so wiring a sink is opt-in and adds no required dependency.
The events map naturally to counters; combine them with Runtime.PoolStats and Runtime.Stats (gauges) for a full picture. Since: 2026-06-07
type PoolStat ¶
type PoolStat struct {
Key string
DisplayVersion string // human label
VersionShort string // first 8 chars of the engine version (hash)
Idle int
CheckedOut int
}
PoolStat is a per-pool snapshot for monitoring (displayVersion identifies which version is running).
type Runtime ¶
type Runtime struct {
// contains filtered or unexported fields
}
Runtime lazily loads, executes, and reclaims many scripts concurrently and safely. Hot reload is a single model: external notification (Notify) driven drop-and-reload.
Example ¶
ExampleRuntime shows the basic public API: load a script from a SourceLoader, run it from the pooled runtime, and clean up.
package main
import (
"context"
"fmt"
lua "github.com/htcom-code/lua-pure/lua"
luart "github.com/htcom-code/go-lua-perf"
)
func main() {
loader := luart.NewMapLoader()
src := `function greet(name) return "hello, " .. name end`
loader.Set("greeter", src, luart.HashVersion(src), "1.0.0")
rt := luart.New(loader, luart.Config{MaxStates: 4})
defer rt.Close()
out, err := rt.Run(context.Background(), "greeter", "greet", lua.MkString("luart"))
if err != nil {
panic(err)
}
fmt.Println(out[0].Str())
}
Output: hello, luart
Example (Notify) ¶
ExampleRuntime_notify shows notification-driven hot reload: after the source changes, Notify drops the pool and the next Run uses the new version.
package main
import (
"context"
"fmt"
luart "github.com/htcom-code/go-lua-perf"
)
func main() {
loader := luart.NewMapLoader()
v1 := `function f() return "v1" end`
loader.Set("s", v1, luart.HashVersion(v1), "1.0.0")
rt := luart.New(loader, luart.Config{MaxStates: 4})
defer rt.Close()
ctx := context.Background()
out, _ := rt.Run(ctx, "s", "f")
fmt.Println(out[0].Str())
v2 := `function f() return "v2" end`
loader.Set("s", v2, luart.HashVersion(v2), "2.0.0")
rt.Notify("s", luart.HashVersion(v2), "2.0.0")
out, _ = rt.Run(ctx, "s", "f")
fmt.Println(out[0].Str())
}
Output: v1 v2
func New ¶
func New(loader SourceLoader, cfg Config) *Runtime
New creates a Runtime and starts its janitor. When MaxStates is unset it is derived from the memory budget.
func (*Runtime) Close ¶
func (m *Runtime) Close()
Close stops the janitor and closes all idle States immediately. It does not wait for in-use States — those close when released. Idempotent. For a graceful drain, use Shutdown.
func (*Runtime) CompileCount ¶
CompileCount returns the number of distinct key:version compiled so far (observability).
func (*Runtime) Notify ¶
Notify applies one external change notification. If the key's pool exists and the version differs, that pool is dropped (reloaded on next use); if only the displayVersion differs, just the label is refreshed (no cold start). If the pool is absent (unused script) it is a no-op. In-flight States keep running on the old version after a drop and are discarded on release.
func (*Runtime) NotifyChanges ¶
NotifyChanges applies several change notifications at once.
func (*Runtime) PoolStats ¶
PoolStats returns a human-facing snapshot of the live pools (for monitoring — which key runs which displayVersion). Dropped pools are removed from the map and therefore not included.
func (*Runtime) Run ¶
func (m *Runtime) Run(ctx context.Context, key, entryFn string, args ...lua.Value) ([]lua.Value, error)
Run executes the key script's entryFn with args (lazily loading, compiling, and creating the pool if needed) and returns its results.
The returned []lua.Value is only safe to use synchronously, before the next pool operation. Scalars and strings are by-value and always safe, but reference types (table/function/userdata) belong to a pooled State that another goroutine may reuse the instant Run returns — reading or calling them afterwards is a data race. Extract what you need immediately, or prefer the safe paths: RunValues (deep-copies results to Go values) or RunWith (borrows results within the State's ownership window). This raw form stays the fastest path for primitive returns (e.g. a JSON string).
func (*Runtime) RunValues ¶
func (m *Runtime) RunValues(ctx context.Context, key, entryFn string, args ...lua.Value) ([]any, error)
RunValues executes the key script's entryFn and deep-copies its results into Go values, so the caller never holds a value tied to a pooled State (the safe, convenient counterpart to Run). It runs the copy inside RunWith, while the State still owns the values.
Conversion: nil→nil, boolean→bool, integer→int64, float→float64, string→string, table→[]any for a clean 1..n sequence or map[any]any otherwise (recursive; map keys must be scalar/string). function/userdata/thread are delegated to Config.ConvertValue, or return an error when it is unset (so a State-bound value is never handed back).
func (*Runtime) RunWith ¶
func (m *Runtime) RunWith(ctx context.Context, key, entryFn string, handle func(L *lua.LState, rets []lua.Value) error, args ...lua.Value) error
RunWith executes the key script's entryFn with args and invokes handle with the running State and the call's results while the State is still owned by this call (before it returns to the pool). Inside handle any return type is safe to read — table/function/userdata included — and returned functions may be called on L.
Do not let L or rets escape handle: once handle returns, the State may be reused by another goroutine, so anything still pointing into it races. Move out whatever you need inside handle (copy values, call returned functions). handle's error is returned as-is; handle is not called if the script itself errors.
func (*Runtime) Shutdown ¶
Shutdown is a graceful Close: it stops the janitor, closes idle States, then waits for in-flight States to be released (each closes on release) until none remain or ctx is done. Returns ctx.Err() on timeout (any still-in-flight States close on their eventual release, so there is no leak). Idempotent and safe to call alongside Close. Mirrors net/http.Server's Close/Shutdown split. Since: 2026-06-07
type SourceLoader ¶
SourceLoader abstracts where script sources come from (an external cache or service backed by a DB, etc.). version is the engine-facing change key (a content hash is recommended) — it must change whenever the source changes. displayVersion is a human-facing label (e.g. "1.0.0"); when empty, callers fall back to the hash prefix.
type TraceHook ¶
TraceHook receives per-request, per-stage timing for developer profiling. It is distinct from Metrics (which is for production aggregation): TraceHook is a request-level profiler for development. Stages are "load" (external fetch), "compile", "acquire" (idle reuse / wait / LRU, includes build if one happened), "build" (NewState + preload), "execute" (the call), and "release".
It is invoked only when Config.Trace is non-nil, so leaving it unset has zero overhead (no time.Now calls). The hook must be safe for concurrent use. Since: 2026-06-07
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
bytecode-cache
command
|
|
|
dynamic-registry
command
Command dynamic-registry is a thin demo of the reusable luart runtime: lazy load from an external source, per-script preloaded VM pools, TTL idle eviction, memory-capped pool size, and notification-driven hot reload (drop-and-reload) with content-hash versions + human-readable displayVersion.
|
Command dynamic-registry is a thin demo of the reusable luart runtime: lazy load from an external source, per-script preloaded VM pools, TTL idle eviction, memory-capped pool size, and notification-driven hot reload (drop-and-reload) with content-hash versions + human-readable displayVersion. |
|
multi-script
command
Command multi-script demonstrates a thread-safe runtime that runs MANY Lua scripts from MANY goroutines, addressing two concerns the simpler bytecode-cache / pool-preload examples leave open:
|
Command multi-script demonstrates a thread-safe runtime that runs MANY Lua scripts from MANY goroutines, addressing two concerns the simpler bytecode-cache / pool-preload examples leave open: |
|
pool-preload
command
|
|
|
examples
|
|
|
basics
command
Example basics is the smallest end-to-end use of the luart library: register a script with a SourceLoader, create a Runtime, and Run a Lua function with an argument, reading back its return value.
|
Example basics is the smallest end-to-end use of the luart library: register a script with a SourceLoader, create a Runtime, and Run a Lua function with an argument, reading back its return value. |
|
config-loading
command
Example config-loading shows building a luart.Config from external sources via the luartconfig subpackage: a base JSON string overlaid by environment variables, using the precedence resolver (env > base > defaults).
|
Example config-loading shows building a luart.Config from external sources via the luartconfig subpackage: a base JSON string overlaid by environment variables, using the precedence resolver (env > base > defaults). |
|
custom-libs
command
Example custom-libs adds a user-authored library to pooled States via Config.ExtraLibs — without restating the sandbox defaults.
|
Example custom-libs adds a user-authored library to pooled States via Config.ExtraLibs — without restating the sandbox defaults. |
|
custom-loaders
command
Example custom-loaders shows how to implement the luart.SourceLoader interface for real backends — a file tree, a database, an in-memory store — plus two hybrid patterns (a caching wrapper and a prefix router).
|
Example custom-loaders shows how to implement the luart.SourceLoader interface for real backends — a file tree, a database, an in-memory store — plus two hybrid patterns (a caching wrapper and a prefix router). |
|
exec-timeout
command
Example exec-timeout shows two ways a runaway script is stopped: a server-side hard cap via Config.ExecTimeout, and a caller deadline via the context passed to Run.
|
Example exec-timeout shows two ways a runaway script is stopped: a server-side hard cap via Config.ExecTimeout, and a caller deadline via the context passed to Run. |
|
graceful-shutdown
command
Example graceful-shutdown shows the Close vs Shutdown split (mirroring net/http.Server): Close stops immediately, Shutdown drains in-flight calls first (up to the ctx deadline).
|
Example graceful-shutdown shows the Close vs Shutdown split (mirroring net/http.Server): Close stops immediately, Shutdown drains in-flight calls first (up to the ctx deadline). |
|
hot-reload
command
Example hot-reload shows notification-driven drop-and-reload: when a script's source changes, Notify drops that script's pool so the next Run picks up the new version — with no Runtime restart.
|
Example hot-reload shows notification-driven drop-and-reload: when a script's source changes, Notify drops that script's pool so the next Run picks up the new version — with no Runtime restart. |
|
logging
command
Example logging shows routing luart's structured events into log/slog via Config.Logger + NewSlogLogger.
|
Example logging shows routing luart's structured events into log/slog via Config.Logger + NewSlogLogger. |
|
memory-budget
command
Example memory-budget shows capping total live VMs by a memory budget instead of a fixed count: with Config.MaxStates unset, the Runtime derives MaxStates from MemoryBudgetBytes ÷ measured per-state cost, then enforces it with LRU eviction + back-pressure under load.
|
Example memory-budget shows capping total live VMs by a memory budget instead of a fixed count: with Config.MaxStates unset, the Runtime derives MaxStates from MemoryBudgetBytes ÷ measured per-state cost, then enforces it with LRU eviction + back-pressure under load. |
|
metrics
command
Example metrics shows wiring a Config.Metrics sink: the Runtime calls OnCompile/OnBuild/OnReuse/OnEvict/OnDrop at lifecycle points.
|
Example metrics shows wiring a Config.Metrics sink: the Runtime calls OnCompile/OnBuild/OnReuse/OnEvict/OnDrop at lifecycle points. |
|
observability
command
Example observability shows the read-only introspection API: Stats (global gauges), PoolStats (per-script snapshot incl.
|
Example observability shows the read-only introspection API: Stats (global gauges), PoolStats (per-script snapshot incl. |
|
sandbox-libs
command
Example sandbox-libs shows controlling which Lua standard libraries a script can reach via Config.Libs.
|
Example sandbox-libs shows controlling which Lua standard libraries a script can reach via Config.Libs. |
|
trace-profiling
command
Example trace-profiling shows Config.Trace: a TraceHook receives per-request, per-stage timings (load, compile, acquire, build, execute, release).
|
Example trace-profiling shows Config.Trace: a TraceHook receives per-request, per-stage timings (load, compile, acquire, build, execute, release). |
|
ttl-eviction
command
Example ttl-eviction shows the background janitor reclaiming idle script pools: a pool unused for longer than Config.IdleTTL is evicted whole (its pooled States closed), freeing memory automatically.
|
Example ttl-eviction shows the background janitor reclaiming idle script pools: a pool unused for longer than Config.IdleTTL is evicted whole (its pooled States closed), freeing memory automatically. |
|
internal
|
|
|
genconfig
command
Command genconfig writes the example luart config files (YAML + JSON) used by `make config`.
|
Command genconfig writes the example luart config files (YAML + JSON) used by `make config`. |
|
Package luartconfig loads a luart.Config from JSON, YAML, or environment variables.
|
Package luartconfig loads a luart.Config from JSON, YAML, or environment variables. |
|
Command performance compares Lua script compile cost between lua-pure (the pure-Go engine luart is built on) and PUC-Lua (the reference C implementation, the `lua` binary on PATH), on identical source files.
|
Command performance compares Lua script compile cost between lua-pure (the pure-Go engine luart is built on) and PUC-Lua (the reference C implementation, the `lua` binary on PATH), on identical source files. |