workers 2
This commit is contained in:
parent
e95babaa25
commit
6395b3ee58
107
core/workers/README.md
Normal file
107
core/workers/README.md
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
# Worker Pool
|
||||||
|
|
||||||
|
### Pool
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Pool struct { ... }
|
||||||
|
|
||||||
|
// Create a pool with specified number of workers
|
||||||
|
func NewPool(numWorkers int) (*Pool, error)
|
||||||
|
|
||||||
|
// Submit a job with default context
|
||||||
|
func (p *Pool) Submit(bytecode []byte, ctx *Context) (any, error)
|
||||||
|
|
||||||
|
// Submit with timeout/cancellation support
|
||||||
|
func (p *Pool) SubmitWithContext(ctx context.Context, bytecode []byte, execCtx *Context) (any, error)
|
||||||
|
|
||||||
|
// Shutdown the pool
|
||||||
|
func (p *Pool) Shutdown() error
|
||||||
|
|
||||||
|
// Get number of active workers
|
||||||
|
func (p *Pool) ActiveWorkers() uint32
|
||||||
|
```
|
||||||
|
|
||||||
|
### Context
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Context struct { ... }
|
||||||
|
|
||||||
|
// Create a new execution context
|
||||||
|
func NewContext() *Context
|
||||||
|
|
||||||
|
// Set a value
|
||||||
|
func (c *Context) Set(key string, value any)
|
||||||
|
|
||||||
|
// Get a value
|
||||||
|
func (c *Context) Get(key string) any
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create worker pool
|
||||||
|
pool, err := workers.NewPool(4)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer pool.Shutdown()
|
||||||
|
|
||||||
|
// Compile bytecode (typically done once and reused)
|
||||||
|
state := luajit.New()
|
||||||
|
bytecode, err := state.CompileBytecode(`
|
||||||
|
return ctx.message .. " from Lua"
|
||||||
|
`, "script")
|
||||||
|
state.Close()
|
||||||
|
|
||||||
|
// Set up execution context
|
||||||
|
ctx := workers.NewContext()
|
||||||
|
ctx.Set("message", "Hello")
|
||||||
|
ctx.Set("params", map[string]any{"id": "123"})
|
||||||
|
|
||||||
|
// Execute bytecode
|
||||||
|
result, err := pool.Submit(bytecode, ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(result) // "Hello from Lua"
|
||||||
|
```
|
||||||
|
|
||||||
|
## With Timeout
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create context with timeout
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Execute with timeout
|
||||||
|
result, err := pool.SubmitWithContext(ctx, bytecode, execCtx)
|
||||||
|
if err != nil {
|
||||||
|
// Handle timeout or error
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## In Lua Scripts
|
||||||
|
|
||||||
|
Inside Lua, the context is available as the global `ctx` table:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
-- Access a simple value
|
||||||
|
local msg = ctx.message
|
||||||
|
|
||||||
|
-- Access nested values
|
||||||
|
local id = ctx.params.id
|
||||||
|
|
||||||
|
-- Return a result to Go
|
||||||
|
return {
|
||||||
|
status = "success",
|
||||||
|
data = msg
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- The pool is thread-safe; multiple goroutines can submit jobs concurrently
|
||||||
|
- Each execution is isolated; global state is reset between executions
|
||||||
|
- Bytecode should be compiled once and reused for better performance
|
||||||
|
- Context values should be serializable to Lua (numbers, strings, booleans, maps, slices)
|
24
core/workers/context.go
Normal file
24
core/workers/context.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package workers
|
||||||
|
|
||||||
|
// Context represents execution context for a Lua script
|
||||||
|
type Context struct {
|
||||||
|
// Generic map for any context values (route params, HTTP request info, etc.)
|
||||||
|
Values map[string]any
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContext creates a new context with initialized maps
|
||||||
|
func NewContext() *Context {
|
||||||
|
return &Context{
|
||||||
|
Values: make(map[string]any),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds a value to the context
|
||||||
|
func (c *Context) Set(key string, value any) {
|
||||||
|
c.Values[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get retrieves a value from the context
|
||||||
|
func (c *Context) Get(key string) any {
|
||||||
|
return c.Values[key]
|
||||||
|
}
|
|
@ -9,6 +9,6 @@ type JobResult struct {
|
||||||
// job represents a Lua script execution request
|
// job represents a Lua script execution request
|
||||||
type job struct {
|
type job struct {
|
||||||
Bytecode []byte // Compiled LuaJIT bytecode
|
Bytecode []byte // Compiled LuaJIT bytecode
|
||||||
Params map[string]any // Parameters to pass to the script
|
Context *Context // Execution context
|
||||||
Result chan<- JobResult // Channel to send result back
|
Result chan<- JobResult // Channel to send result back
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,7 @@ func NewPool(numWorkers int) (*Pool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// SubmitWithContext sends a job to the worker pool with context
|
// SubmitWithContext sends a job to the worker pool with context
|
||||||
func (p *Pool) SubmitWithContext(ctx context.Context, bytecode []byte, params map[string]any) (any, error) {
|
func (p *Pool) SubmitWithContext(ctx context.Context, bytecode []byte, execCtx *Context) (any, error) {
|
||||||
if !p.isRunning.Load() {
|
if !p.isRunning.Load() {
|
||||||
return nil, ErrPoolClosed
|
return nil, ErrPoolClosed
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ func (p *Pool) SubmitWithContext(ctx context.Context, bytecode []byte, params ma
|
||||||
resultChan := make(chan JobResult, 1)
|
resultChan := make(chan JobResult, 1)
|
||||||
j := job{
|
j := job{
|
||||||
Bytecode: bytecode,
|
Bytecode: bytecode,
|
||||||
Params: params,
|
Context: execCtx,
|
||||||
Result: resultChan,
|
Result: resultChan,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,6 +73,11 @@ func (p *Pool) SubmitWithContext(ctx context.Context, bytecode []byte, params ma
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Submit sends a job to the worker pool
|
||||||
|
func (p *Pool) Submit(bytecode []byte, execCtx *Context) (any, error) {
|
||||||
|
return p.SubmitWithContext(context.Background(), bytecode, execCtx)
|
||||||
|
}
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the worker pool
|
// Shutdown gracefully shuts down the worker pool
|
||||||
func (p *Pool) Shutdown() error {
|
func (p *Pool) Shutdown() error {
|
||||||
if !p.isRunning.Load() {
|
if !p.isRunning.Load() {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package workers
|
package workers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"errors"
|
"errors"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
|
|
||||||
|
@ -102,13 +101,17 @@ func (w *worker) resetState() {
|
||||||
w.state.DoString("__reset_globals()")
|
w.state.DoString("__reset_globals()")
|
||||||
}
|
}
|
||||||
|
|
||||||
// setParams sets job parameters as a global 'params' table
|
// setContext sets job context as global tables in Lua state
|
||||||
func (w *worker) setParams(params map[string]any) error {
|
func (w *worker) setContext(ctx *Context) error {
|
||||||
// Create new table for params
|
if ctx == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create context table
|
||||||
w.state.NewTable()
|
w.state.NewTable()
|
||||||
|
|
||||||
// Add each parameter to the table
|
// Add values to context table
|
||||||
for key, value := range params {
|
for key, value := range ctx.Values {
|
||||||
// Push key
|
// Push key
|
||||||
w.state.PushString(key)
|
w.state.PushString(key)
|
||||||
|
|
||||||
|
@ -121,8 +124,8 @@ func (w *worker) setParams(params map[string]any) error {
|
||||||
w.state.SetTable(-3)
|
w.state.SetTable(-3)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the table as global 'params'
|
// Set the table as global 'ctx'
|
||||||
w.state.SetGlobal("params")
|
w.state.SetGlobal("ctx")
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -132,9 +135,9 @@ func (w *worker) executeJob(j job) JobResult {
|
||||||
// Reset state before execution
|
// Reset state before execution
|
||||||
w.resetState()
|
w.resetState()
|
||||||
|
|
||||||
// Set parameters
|
// Set context
|
||||||
if j.Params != nil {
|
if j.Context != nil {
|
||||||
if err := w.setParams(j.Params); err != nil {
|
if err := w.setContext(j.Context); err != nil {
|
||||||
return JobResult{nil, err}
|
return JobResult{nil, err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,8 +158,3 @@ func (w *worker) executeJob(j job) JobResult {
|
||||||
|
|
||||||
return JobResult{value, err}
|
return JobResult{value, err}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit sends a job to the worker pool
|
|
||||||
func (p *Pool) Submit(bytecode []byte, params map[string]any) (any, error) {
|
|
||||||
return p.SubmitWithContext(context.Background(), bytecode, params)
|
|
||||||
}
|
|
||||||
|
|
|
@ -127,10 +127,10 @@ func TestPoolSubmitWithContext(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to make sure we can pass different types of parameters from Go to Lua and
|
// We need to make sure we can pass different types of context values from Go to Lua and
|
||||||
// get them back properly. This test sends numbers, strings, booleans, and arrays to
|
// get them back properly. This test sends numbers, strings, booleans, and arrays to
|
||||||
// a Lua script and verifies they're all handled correctly in both directions.
|
// a Lua script and verifies they're all handled correctly in both directions.
|
||||||
func TestJobParameters(t *testing.T) {
|
func TestContextValues(t *testing.T) {
|
||||||
pool, err := NewPool(2)
|
pool, err := NewPool(2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to create pool: %v", err)
|
t.Fatalf("Failed to create pool: %v", err)
|
||||||
|
@ -139,21 +139,20 @@ func TestJobParameters(t *testing.T) {
|
||||||
|
|
||||||
bytecode := createTestBytecode(t, `
|
bytecode := createTestBytecode(t, `
|
||||||
return {
|
return {
|
||||||
num = params.number,
|
num = ctx.number,
|
||||||
str = params.text,
|
str = ctx.text,
|
||||||
flag = params.enabled,
|
flag = ctx.enabled,
|
||||||
list = {params.table[1], params.table[2], params.table[3]},
|
list = {ctx.table[1], ctx.table[2], ctx.table[3]},
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
params := map[string]any{
|
execCtx := NewContext()
|
||||||
"number": 42.5,
|
execCtx.Set("number", 42.5)
|
||||||
"text": "hello",
|
execCtx.Set("text", "hello")
|
||||||
"enabled": true,
|
execCtx.Set("enabled", true)
|
||||||
"table": []float64{10, 20, 30},
|
execCtx.Set("table", []float64{10, 20, 30})
|
||||||
}
|
|
||||||
|
|
||||||
result, err := pool.Submit(bytecode, params)
|
result, err := pool.Submit(bytecode, execCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Failed to submit job: %v", err)
|
t.Fatalf("Failed to submit job: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -188,6 +187,64 @@ func TestJobParameters(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test context with nested data structures
|
||||||
|
func TestNestedContext(t *testing.T) {
|
||||||
|
pool, err := NewPool(2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create pool: %v", err)
|
||||||
|
}
|
||||||
|
defer pool.Shutdown()
|
||||||
|
|
||||||
|
bytecode := createTestBytecode(t, `
|
||||||
|
return {
|
||||||
|
id = ctx.params.id,
|
||||||
|
name = ctx.params.name,
|
||||||
|
method = ctx.request.method,
|
||||||
|
path = ctx.request.path
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
execCtx := NewContext()
|
||||||
|
|
||||||
|
// Set nested params
|
||||||
|
params := map[string]any{
|
||||||
|
"id": "123",
|
||||||
|
"name": "test",
|
||||||
|
}
|
||||||
|
execCtx.Set("params", params)
|
||||||
|
|
||||||
|
// Set nested request info
|
||||||
|
request := map[string]any{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/test",
|
||||||
|
}
|
||||||
|
execCtx.Set("request", request)
|
||||||
|
|
||||||
|
result, err := pool.Submit(bytecode, execCtx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to submit job: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result should be a map
|
||||||
|
resultMap, ok := result.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Expected map result, got %T", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resultMap["id"] != "123" {
|
||||||
|
t.Errorf("Expected id=123, got %v", resultMap["id"])
|
||||||
|
}
|
||||||
|
if resultMap["name"] != "test" {
|
||||||
|
t.Errorf("Expected name=test, got %v", resultMap["name"])
|
||||||
|
}
|
||||||
|
if resultMap["method"] != "GET" {
|
||||||
|
t.Errorf("Expected method=GET, got %v", resultMap["method"])
|
||||||
|
}
|
||||||
|
if resultMap["path"] != "/api/test" {
|
||||||
|
t.Errorf("Expected path=/api/test, got %v", resultMap["path"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A key requirement for our worker pool is that we don't leak state between executions.
|
// A key requirement for our worker pool is that we don't leak state between executions.
|
||||||
// This test confirms that by setting a global variable in one job and then checking
|
// This test confirms that by setting a global variable in one job and then checking
|
||||||
// that it's been cleared before the next job runs on the same worker.
|
// that it's been cleared before the next job runs on the same worker.
|
||||||
|
@ -261,7 +318,7 @@ func TestPoolShutdown(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// A robust worker pool needs to handle errors gracefully. This test checks various
|
// A robust worker pool needs to handle errors gracefully. This test checks various
|
||||||
// error scenarios: invalid bytecode, Lua runtime errors, nil parameters (which
|
// error scenarios: invalid bytecode, Lua runtime errors, nil context (which
|
||||||
// should work fine), and unsupported parameter types (which should properly error out).
|
// should work fine), and unsupported parameter types (which should properly error out).
|
||||||
func TestErrorHandling(t *testing.T) {
|
func TestErrorHandling(t *testing.T) {
|
||||||
pool, err := NewPool(2)
|
pool, err := NewPool(2)
|
||||||
|
@ -287,25 +344,24 @@ func TestErrorHandling(t *testing.T) {
|
||||||
t.Errorf("Expected error from Lua error() call, got nil")
|
t.Errorf("Expected error from Lua error() call, got nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test invalid parameter
|
// Test with nil context
|
||||||
bytecode = createTestBytecode(t, "return param")
|
bytecode = createTestBytecode(t, "return ctx == nil")
|
||||||
|
result, err := pool.Submit(bytecode, nil)
|
||||||
// This should work with nil value
|
|
||||||
_, err = pool.Submit(bytecode, map[string]any{
|
|
||||||
"param": nil,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Unexpected error with nil param: %v", err)
|
t.Errorf("Unexpected error with nil context: %v", err)
|
||||||
|
}
|
||||||
|
if result.(bool) != true {
|
||||||
|
t.Errorf("Expected ctx to be nil in Lua, but it wasn't")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complex type that can't be converted
|
// Test invalid context value
|
||||||
complex := map[string]any{
|
execCtx := NewContext()
|
||||||
"param": complex128(1 + 2i), // Unsupported type
|
execCtx.Set("param", complex128(1+2i)) // Unsupported type
|
||||||
}
|
|
||||||
|
|
||||||
_, err = pool.Submit(bytecode, complex)
|
bytecode = createTestBytecode(t, "return ctx.param")
|
||||||
|
_, err = pool.Submit(bytecode, execCtx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("Expected error for unsupported parameter type, got nil")
|
t.Errorf("Expected error for unsupported context value type, got nil")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,15 +379,17 @@ func TestConcurrentExecution(t *testing.T) {
|
||||||
defer pool.Shutdown()
|
defer pool.Shutdown()
|
||||||
|
|
||||||
// Create bytecode that returns its input
|
// Create bytecode that returns its input
|
||||||
bytecode := createTestBytecode(t, "return params.n")
|
bytecode := createTestBytecode(t, "return ctx.n")
|
||||||
|
|
||||||
// Run multiple jobs concurrently
|
// Run multiple jobs concurrently
|
||||||
results := make(chan int, jobs)
|
results := make(chan int, jobs)
|
||||||
for i := 0; i < jobs; i++ {
|
for i := 0; i < jobs; i++ {
|
||||||
i := i // Capture loop variable
|
i := i // Capture loop variable
|
||||||
go func() {
|
go func() {
|
||||||
params := map[string]any{"n": float64(i)}
|
execCtx := NewContext()
|
||||||
result, err := pool.Submit(bytecode, params)
|
execCtx.Set("n", float64(i))
|
||||||
|
|
||||||
|
result, err := pool.Submit(bytecode, execCtx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("Job %d failed: %v", i, err)
|
t.Errorf("Job %d failed: %v", i, err)
|
||||||
results <- -1
|
results <- -1
|
||||||
|
@ -363,3 +421,25 @@ func TestConcurrentExecution(t *testing.T) {
|
||||||
t.Errorf("Expected %d unique results, got %d", jobs, len(counts))
|
t.Errorf("Expected %d unique results, got %d", jobs, len(counts))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test context operations
|
||||||
|
func TestContext(t *testing.T) {
|
||||||
|
ctx := NewContext()
|
||||||
|
|
||||||
|
// Test Set and Get
|
||||||
|
ctx.Set("key", "value")
|
||||||
|
if ctx.Get("key") != "value" {
|
||||||
|
t.Errorf("Expected value, got %v", ctx.Get("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test overwriting
|
||||||
|
ctx.Set("key", 123)
|
||||||
|
if ctx.Get("key") != 123 {
|
||||||
|
t.Errorf("Expected 123, got %v", ctx.Get("key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test missing key
|
||||||
|
if ctx.Get("missing") != nil {
|
||||||
|
t.Errorf("Expected nil for missing key, got %v", ctx.Get("missing"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user