упростил

main v0.0.15
parent 89391e67d8
commit 9d7798196b
  1. 90
      ottomain.go

@ -1,7 +1,6 @@
package ottoman package ottoman
import ( import (
"context"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -14,8 +13,9 @@ import (
) )
type otto_config struct { type otto_config struct {
Timeout int `env:"JS_TIMEOUT" envDefault:"2"` Timeout time.Duration `env:"JS_TIMEOUT" envDefault:"2s"`
MaxConcurrent int `env:"JS_MAX_CONCURRENT" envDefault:"50"` MaxConcurrent int `env:"JS_MAX_CONCURRENT" envDefault:"50"`
WaitTimeout time.Duration `env:"JS_MAX_WAITTIMEOUT" envDefault:"30s"`
} }
var ( var (
@ -26,6 +26,8 @@ var (
configMutex sync.Mutex configMutex sync.Mutex
) )
var ErrTimeout = errors.New("javascript execution timeout")
func jsBtoa(b string) string { func jsBtoa(b string) string {
return base64.StdEncoding.EncodeToString([]byte(b)) return base64.StdEncoding.EncodeToString([]byte(b))
} }
@ -59,9 +61,9 @@ func initSemaphore() {
if !configChecked { if !configChecked {
var cfg otto_config var cfg otto_config
if err := env.Parse(&cfg); err != nil { if err := env.Parse(&cfg); err != nil {
// Use defaults if env parse fails
cfg.MaxConcurrent = 50 cfg.MaxConcurrent = 50
cfg.Timeout = 2 cfg.Timeout = 2 * time.Second
cfg.WaitTimeout = 30 * time.Second
} }
globalConfig = cfg globalConfig = cfg
semaphore = make(chan struct{}, cfg.MaxConcurrent) semaphore = make(chan struct{}, cfg.MaxConcurrent)
@ -70,26 +72,22 @@ func initSemaphore() {
} }
func ProcessRequest(script string, params map[string]interface{}) (response map[string]interface{}, err error) { func ProcessRequest(script string, params map[string]interface{}) (response map[string]interface{}, err error) {
// Initialize semaphore and config once
once.Do(initSemaphore) once.Do(initSemaphore)
// Acquire semaphore to limit concurrent executions cfg := globalConfig
select { select {
case semaphore <- struct{}{}: case semaphore <- struct{}{}:
defer func() { <-semaphore }() // Release semaphore when done defer func() { <-semaphore }()
case <-time.After(30 * time.Second): case <-time.After(cfg.WaitTimeout):
// Timeout if too many concurrent requests return nil, errors.New("too many concurrent JavaScript executions, please try again later")
return nil, fmt.Errorf("too many concurrent JavaScript executions, please try again later")
} }
// Use cached config instead of parsing every time
cfg := globalConfig
vm := otto.New() vm := otto.New()
err = jsRegisterFunctions(vm) err = jsRegisterFunctions(vm)
if err != nil { if err != nil {
return nil, fmt.Errorf("otto registreing standart functions error: %w", err) return nil, fmt.Errorf("otto registering standard functions error: %w", err)
} }
for key, uf := range params { for key, uf := range params {
@ -99,64 +97,20 @@ func ProcessRequest(script string, params map[string]interface{}) (response map[
} }
} }
vm.Interrupt = make(chan func(), 1) timer := time.AfterFunc(cfg.Timeout, func() {
timeoutCtx, timeoutCancel := context.WithCancel(context.Background()) vm.Interrupt <- func() {
timeoutTimer := time.NewTimer(time.Duration(cfg.Timeout) * time.Second) panic(ErrTimeout)
// Start timeout goroutine
go func() {
select {
case <-timeoutTimer.C:
// Timer expired, try to interrupt (non-blocking)
select {
case vm.Interrupt <- func() {
panic(errors.New("some code took to long! Stopping after timeout"))
}:
// Interrupt sent successfully
case <-timeoutCtx.Done():
// Script already completed, cancel prevented interrupt
default:
// Can't send interrupt (channel full), but context cancelled
// Just exit - script might be done anyway
}
case <-timeoutCtx.Done():
// Script completed before timeout, stop timer
if !timeoutTimer.Stop() {
// Timer already fired, drain it
select {
case <-timeoutTimer.C:
default:
}
}
} }
}() })
defer timer.Stop()
defer func() {
// Cancel timeout context to signal goroutine to exit
timeoutCancel()
// Stop timer - this is safe even if it already fired
timeoutTimer.Stop()
// Drain timer channel to prevent goroutine leak
select {
case <-timeoutTimer.C:
default:
}
if r := recover(); r != nil {
switch x := r.(type) {
case error:
err = x
default:
err = errors.New("otto run error")
}
response = nil
}
}()
_, err = vm.Run(script) _, err = vm.Run(script)
if err != nil { if err != nil {
if errors.Is(err, ErrTimeout) {
return nil, ErrTimeout
}
return nil, fmt.Errorf("otto run error: %w", err) return nil, fmt.Errorf("otto run error: %w", err)
} }

Loading…
Cancel
Save