refactor: 取 VM 的三条路径合一,newVM 成为唯一入口
borrow(池化)、ensure(实例)、borrowStatic(静态)各自写了一遍「取作用域 →
拼 extra → 建 Runtime → load」。borrowStatic 甚至把 newVM 逐行抄了一遍,只为把
noInstance 传成 true。
结果就是规则会漏:上一个 commit 修的「borrowStatic 没标 scoped」正是这么来的。
现在三条都走 newVM,作用域注入和 scoped 标记只有一份。
顺带删掉两处无意义的间接:
loadInto s.load(ctx, rt, ctorArgs, false) 的单行包装,只被 newVM 调一次
newRuntime 它的 scope 参数两个调用点都传 s.name,而注释描述的「会话 VM 用
会话 key」这条路径根本没实现(作用域 key 走的是 vmGlobals)。
参数名还跟同包的 type scope struct 撞名
**scope 是显式参数,不是从 ctx 嗅的**——这一点差点被我改坏:Instance 重建 VM
时要回到它**创建时**那个作用域,而不是当次调用 ctx 里的。对一个作用域实例调
Call(context.Background()) 不该把它的扩展弄丢。加了
TestScope_实例重建VM后仍在原作用域 守这条,用超时逼出 VM 重建(第一版用抛异常,
那不会丢 VM,两种写法都通过,等于没测)。
This commit is contained in:
+3
-6
@@ -119,12 +119,9 @@ func (i *Instance) ensure(ctx context.Context) error {
|
|||||||
if i.vm != nil {
|
if i.vm != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
// 作用域带来的额外全局(扩展 + ScopeGlobals)现取现用,不在 Instance 上留副本。
|
// 用 i.scope 而不是 ctx 里的:实例属于它**创建时**那个作用域,
|
||||||
var extra map[string]any
|
// 重建 VM 时要回到那里去,不能跟着当次调用走
|
||||||
if i.scope != nil {
|
vm, err := i.script.newVM(ctx, i.scope, i.ctorArgs, false)
|
||||||
extra = i.scope.vmGlobals()
|
|
||||||
}
|
|
||||||
vm, err := i.script.newVM(ctx, extra, i.ctorArgs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,3 +431,49 @@ func toNumber(t *testing.T, v any) float64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 作用域实例重建 VM 时,要回到它**创建时**那个作用域。
|
||||||
|
//
|
||||||
|
// Instance 的 VM 超时后会被丢弃,下次调用重建(见 Resets)。重建走的是 i.scope,
|
||||||
|
// 不是当次调用 ctx 里的——对一个作用域实例调 Call(context.Background()) 不该把
|
||||||
|
// 它的扩展弄丢。
|
||||||
|
//
|
||||||
|
// 这条守的是 newVM 的 scope 参数为什么是显式传的:改成从 ctx 嗅探就会破坏它。
|
||||||
|
func TestScope_实例重建VM后仍在原作用域(t *testing.T) {
|
||||||
|
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
|
||||||
|
s := mustCompile(t, e, "s.ts", `
|
||||||
|
export default class S {
|
||||||
|
spin() { while (true) {} }
|
||||||
|
seeStore() { return typeof store }
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
|
||||||
|
st := testext.New()
|
||||||
|
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
|
||||||
|
obj, err := s.New(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer obj.Close()
|
||||||
|
|
||||||
|
if got, _ := obj.Call(ctx, "seeStore"); got != "object" {
|
||||||
|
t.Fatalf("一开始就没看见扩展: %#v", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超时会让实例丢掉 VM,下次调用重建
|
||||||
|
if _, err := obj.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) {
|
||||||
|
t.Fatalf("该超时: %v", err)
|
||||||
|
}
|
||||||
|
if obj.Resets() != 1 {
|
||||||
|
t.Fatalf("VM 没被丢弃,这条测不到重建(Resets=%d)", obj.Resets())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用**不带作用域**的 ctx 触发重建:实例仍属于原来那个作用域
|
||||||
|
got, err := obj.Call(context.Background(), "seeStore")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != "object" {
|
||||||
|
t.Errorf("重建之后扩展丢了(%#v)——实例被当成了无作用域的", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -112,22 +112,16 @@ func (s *Script) Close() {
|
|||||||
// 代价是这类调用每次约 5μs 建一个 VM。同一作用域下要反复调,用 New 拿实例更划算——
|
// 代价是这类调用每次约 5μs 建一个 VM。同一作用域下要反复调,用 New 拿实例更划算——
|
||||||
// 实例把 VM 攥在手里,不必每次重建。
|
// 实例把 VM 攥在手里,不必每次重建。
|
||||||
func (s *Script) borrow(ctx context.Context) (*vmHandle, error) {
|
func (s *Script) borrow(ctx context.Context) (*vmHandle, error) {
|
||||||
if sc, ok := scopeOf(ctx); ok {
|
// 有作用域就必然要单造:池里的 VM 没装这个作用域的扩展
|
||||||
// 作用域至少带一个 scope.key,所以只要有作用域就必然要单造
|
sc, scoped := scopeOf(ctx)
|
||||||
vm, err := s.newVM(ctx, sc.vmGlobals(), nil)
|
if !scoped {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
vm.scoped = true
|
|
||||||
return vm, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case inst := <-s.pool:
|
case inst := <-s.pool:
|
||||||
return inst, nil
|
return inst, nil
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
return s.newVM(ctx, nil, nil)
|
}
|
||||||
|
return s.newVM(ctx, sc, nil, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// release 归还 VM。healthy 为 false(被中断过或 panic 过)时直接丢弃:
|
// release 归还 VM。healthy 为 false(被中断过或 panic 过)时直接丢弃:
|
||||||
@@ -148,36 +142,40 @@ func (s *Script) release(inst *vmHandle, healthy bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newVM 造一个新 VM 并在里面加载这个脚本:注入白名单(extra 是会话专属的额外全局,
|
// newVM 造一个新 VM 并在里面加载这个脚本:注入白名单 → 跑一遍脚本顶层代码 →
|
||||||
// 池化调用传 nil)→ 跑一遍脚本顶层代码 → 记下求值结果。
|
// 记下求值结果。
|
||||||
func (s *Script) newVM(ctx context.Context, extra map[string]any, ctorArgs []any) (*vmHandle, error) {
|
//
|
||||||
rt, err := s.engine.newRuntime(s.name, extra)
|
// **取 VM 的唯一入口**。三条路径(池化 borrow、实例 ensure、静态 borrowStatic)
|
||||||
|
// 都走它,作用域注入和 scoped 标记的规则才只有一份——这两件事分散过一次,
|
||||||
|
// 结果就是 borrowStatic 漏标了 scoped。
|
||||||
|
//
|
||||||
|
// sc 是这个 VM 属于哪个作用域,nil 表示不属于任何作用域。**显式传而不是从 ctx 嗅**:
|
||||||
|
// Instance 重建 VM 时要回到它**创建时**那个作用域,而不是当次调用 ctx 里的那个——
|
||||||
|
// 对一个作用域实例调 Call(context.Background()) 不该把它的扩展弄丢。
|
||||||
|
//
|
||||||
|
// ctorArgs 传给 constructor;noInstance 为 true 时不构造实例,只想调静态方法时用,
|
||||||
|
// 避免白跑一遍 constructor(那是每次调用的准备工作)。
|
||||||
|
func (s *Script) newVM(ctx context.Context, sc *scope, ctorArgs []any, noInstance bool) (*vmHandle, error) {
|
||||||
|
var extra map[string]any
|
||||||
|
if sc != nil {
|
||||||
|
extra = sc.vmGlobals()
|
||||||
|
}
|
||||||
|
|
||||||
|
rt := goja.New()
|
||||||
|
if s.engine.maxStack > 0 {
|
||||||
|
rt.SetMaxCallStackSize(s.engine.maxStack)
|
||||||
|
}
|
||||||
|
if err := s.engine.bind(rt, s.name, extra); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vm, err := s.load(ctx, rt, ctorArgs, noInstance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return s.loadInto(ctx, rt, ctorArgs)
|
// 带作用域的 VM 装着那个作用域的扩展,绝不能回池——release 靠这个标志拒绝它
|
||||||
}
|
vm.scoped = sc != nil
|
||||||
|
return vm, nil
|
||||||
// newRuntime 造一个空 VM 并注入白名单。scope 是它在日志和 store 里的身份:
|
|
||||||
// 池化 VM 用脚本名,会话 VM 用会话 key。
|
|
||||||
func (e *Engine) newRuntime(scope string, extra map[string]any) (*goja.Runtime, error) {
|
|
||||||
rt := goja.New()
|
|
||||||
if e.maxStack > 0 {
|
|
||||||
rt.SetMaxCallStackSize(e.maxStack)
|
|
||||||
}
|
|
||||||
if err := e.bind(rt, scope, extra); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return rt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadInto 在一个已经建好的 VM 里跑这个脚本,取出它的导出。
|
|
||||||
// 顶层代码同样受超时保护,脚本在顶层写死循环不会把调用方卡住。
|
|
||||||
//
|
|
||||||
// 同一个 rt 上可以先后加载多个脚本:打包产物是 IIFE,除了 ModuleGlobal 这一个全局名
|
|
||||||
// 之外不往外泄漏东西,而那个名字的值在这里当场就取走了,后一个脚本覆盖它也不影响。
|
|
||||||
func (s *Script) loadInto(ctx context.Context, rt *goja.Runtime, ctorArgs []any) (*vmHandle, error) {
|
|
||||||
return s.load(ctx, rt, ctorArgs, false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// load 把脚本装进一个 VM。noInstance 为 true 时不构造实例——
|
// load 把脚本装进一个 VM。noInstance 为 true 时不构造实例——
|
||||||
|
|||||||
@@ -79,23 +79,7 @@ func (s *Script) borrowStatic(ctx context.Context) (*vmHandle, error) {
|
|||||||
return nil, newError(KindClosed, s.name, "", ErrClosed, "脚本已关闭")
|
return nil, newError(KindClosed, s.name, "", ErrClosed, "脚本已关闭")
|
||||||
}
|
}
|
||||||
|
|
||||||
var extra map[string]any
|
sc, _ := scopeOf(ctx)
|
||||||
if sc, ok := scopeOf(ctx); ok {
|
// noInstance:静态方法挂在 class 上,不用白跑一遍 constructor
|
||||||
extra = sc.vmGlobals()
|
return s.newVM(ctx, sc, nil, true)
|
||||||
}
|
|
||||||
|
|
||||||
rt, err := s.engine.newRuntime(s.name, extra)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
vm, err := s.load(ctx, rt, nil, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// 带作用域的 VM 装着那个作用域的扩展,绝不能回池。今天 staticTarget.finish
|
|
||||||
// 永远丢弃,标不标都不漏;但同一个约束不该靠两套机制守——以后有人把静态 VM
|
|
||||||
// 接进 release,漏标就是一次跨请求泄漏
|
|
||||||
vm.scoped = extra != nil
|
|
||||||
return vm, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user