cluster/router/script/instance/js_instance.go (160 lines of code) (raw):

/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package instance import ( "context" "fmt" "reflect" "sync" "sync/atomic" ) import ( "github.com/dop251/goja" ) import ( "dubbo.apache.org/dubbo-go/v3/protocol" ) const ( jsScriptResultName = `__go_program_result` jsScriptPrefix = "\n" + jsScriptResultName + ` = ` ) type jsInstances struct { insPool *sync.Pool // store *goja.runtime pgLock sync.RWMutex program map[string]*program // rawScript to compiledProgram } type jsInstance struct { rt *goja.Runtime } type program struct { pg *goja.Program count int32 } func newProgram(pg *goja.Program) *program { return &program{ pg: pg, count: 1, } } func (p *program) addCount(i int) int { return int(atomic.AddInt32(&p.count, int32(i))) } func newJsInstances() *jsInstances { return &jsInstances{ program: map[string]*program{}, insPool: &sync.Pool{New: func() any { return newJsInstance() }}, } } func (i *jsInstances) Run(rawScript string, invokers []protocol.Invoker, invocation protocol.Invocation) ([]protocol.Invoker, error) { i.pgLock.RLock() pg, ok := i.program[rawScript] i.pgLock.RUnlock() if !ok || len(invokers) == 0 { return invokers, nil } matcher := i.insPool.Get().(*jsInstance) packInvokers := make([]protocol.Invoker, 0, len(invokers)) for _, invoker := range invokers { packInvokers = append(packInvokers, newScriptInvokerImpl(invoker)) } ctx := invocation.GetAttachmentAsContext() matcher.initCallArgs(packInvokers, invocation, ctx) matcher.initReplyVar() scriptRes, err := matcher.runScript(pg.pg) if err != nil { return invokers, err } invocation.MergeAttachmentFromContext(ctx) rtInvokersArr, ok := scriptRes.(*goja.Object).Export().([]any) if !ok { return invokers, fmt.Errorf("script result is not array , script return type: %s", reflect.ValueOf(scriptRes.(*goja.Object).Export()).String()) } result := make([]protocol.Invoker, 0, len(rtInvokersArr)) for _, res := range rtInvokersArr { if i, ok := res.(*scriptInvokerWrapper); ok { i.setRanMode() result = append(result, res.(protocol.Invoker)) } else { return invokers, fmt.Errorf("invalid element type , it should be (*scriptInvokerWrapper) , but type is : %s)", reflect.TypeOf(res).String()) } } return result, nil } func (i *jsInstances) Compile(rawScript string) error { var ( ok bool pg *program ) i.pgLock.RLock() pg, ok = i.program[rawScript] i.pgLock.RUnlock() if ok { pg.addCount(1) return nil } else { i.pgLock.Lock() defer i.pgLock.Unlock() // double check to avoid race if pg, ok = i.program[rawScript]; ok { pg.addCount(1) } else { newPg, err := goja.Compile("", jsScriptPrefix+rawScript, true) if err != nil { return err } i.program[rawScript] = newProgram(newPg) } return nil } } func (i *jsInstances) Destroy(rawScript string) { i.pgLock.Lock() if pg, ok := i.program[rawScript]; ok { if pg.addCount(-1) == 0 { delete(i.program, rawScript) } } i.pgLock.Unlock() } func (j jsInstance) initCallArgs(invokers []protocol.Invoker, invocation protocol.Invocation, ctx context.Context) { j.rt.ClearInterrupt() err := j.rt.Set(`invokers`, invokers) if err != nil { panic(err) } err = j.rt.Set(`invocation`, invocation) if err != nil { panic(err) } err = j.rt.Set(`context`, ctx) if err != nil { panic(err) } } // must be set, or throw err like `var jsScriptResultName` not define func (j jsInstance) initReplyVar() { err := j.rt.Set(jsScriptResultName, nil) if err != nil { panic(err) } } func newJsInstance() *jsInstance { return &jsInstance{ rt: goja.New(), } } func (j jsInstance) runScript(pg *goja.Program) (res any, err error) { defer func(res *any, err *error) { if panicReason := recover(); panicReason != nil { *res = nil switch panicReason := panicReason.(type) { case string: *err = fmt.Errorf("panic: %s", panicReason) case error: *err = panicReason default: *err = fmt.Errorf("panic occurred: failed to retrieve panic information. Expected string or error, got %T", panicReason) } } }(&res, &err) res, err = j.rt.RunProgram(pg) return res, err }