在DeepSeek Harness插件内核-10:Fiber是如何管理插件生命周期的 介绍Fiber的时候,我曾经提到过Cordis一个关于Context隔离的问题,我们今天就来着重聊聊,看看它是否是Deep Harness的Bug。如果它算一个BUG的话,我觉得这是一个大BuG。
1. 问题重现
我们先利用一个简单的例子来重写这个问题。 如下面的代码所示,我们创建了一个作为根的Context,并采用指定的名称foobar注册了一个函数形式的服务,函数输出字符串foo。然后我们调用Context的isolate方法针对服务名称foobar创建了一个与根Context隔离的子Context,并在子Context上面使用相同的名称注册了另一个函数,后者输出bar。我们验证隔离性,我们直接利用从两个Context的foobar属性提取并执行注册的函数。
import{Context}from'@deepseek-ai/cordis'declaremodule'@deepseek-ai/cordis'{interfaceContext{foobar:()=>void;}}constcontext=newContext();context.provide("foobar",()=>console.log("foo"));constsubContext=context.isolate("foobar");subContext.provide("foobar",()=>console.log("bar"));context.foobar();subContext.foobar();console.log();context.plugin(ctx=>ctx.foobar());subContext.inject(["foobar"],ctx=>ctx.foobar());然后我们在根Context调用plugin方法注册了一个插件,对应的插件函数会直接提取并执行当前Context的foobar属性,并作为函数予以执行。针对子Context,我们则调用inject将foobar服务作为注入到注册的插件中,该插件与注册在根Context的插件具有相同的实现。从如下的输出可以看出:直接调用Context提取的服务函数会输出我们希望的结果,但是通过插件执行的输出则不行。
foo bar bar bar输出为bar的函数注册在子Context上,而且是通过调用isolate针对服务注册名称foobar创建的,但是却没有起到隔离的作用,把注册在根Context上的同名服务给覆盖了。
2. 进一步验证Context上注册的服务是否正确?
我们可以进一步按照DeepSeek Harness插件内核-06:Context全面解析介绍的关于isolate方法执行逻辑看看作为服务标识的Symbol是否正确写入两个Context的symbols.isolate字典中。为此我们利用如下的方式分别从两个Context的symbols.isolate字典中将服务名称foobar对应的Symbol提取出来,然后以此作为Key从RefelectService中提取对应的服务实例并执行。从输出来看毫无问题。
import{Context,symbols}from'@deepseek-ai/cordis'declaremodule'@deepseek-ai/cordis'{interfaceContext{foobar:()=>void;}}constcontext=newContext();context.provide("foobar",()=>console.log("foo"));constsubContext=context.isolate("foobar");subContext.provide("foobar",()=>console.log("bar"));context.foobar();subContext.foobar();console.log();letkey=context[symbols.isolate]["foobar"]assymbol;(context.reflect.store[key]?.valueas()=>void)();key=subContext[symbols.isolate]["foobar"]assymbol;(context.reflect.store[key]?.valueas()=>void)();输出:
foo bar foo bar3. 查看Fiber的服务存储
到目前为止我们可以确认当注册在根Context中的插件在执行的时候,提取foobar服务实例并没有按照我们希望的方式:从根Context的symbols.isolate字典中根据指定的服务注册名称查询出作为服务实例唯一标识的Symbol,再从ReflectService的store这个服务全局注册表中提取对应的封装了服务实例的Impl对象。那么插件执行过程中提取的服务实例来源于何处呢?通过DeepSeek Harness插件内核-08:利用RefectService以反射方式扩展Context的介绍,ReflectService的provide方法还会将服务冗余存储与当前Fiber对象的store字典中,如下的演示程序证实了这一点:
import{Context,symbols}from'@deepseek-ai/cordis'declaremodule'@deepseek-ai/cordis'{interfaceContext{foobar:()=>void;}}constcontext=newContext();context.provide("foobar",()=>console.log("foo"));constsubContext=context.isolate("foobar");subContext.provide("foobar",()=>console.log("bar"));context.foobar();subContext.foobar();(context.fiber.store!["foobar"]?.valueas()=>void)();输出:
foo bar bar而且这也能充分介绍前面的现象:当我们调用isolate方法创建一个子Context的时候,只会为创建的Context指定一个symbols.isolate字典,但是Fiber依然是继承自父Context。所以当我们调用provide方法在子Context上注册同名服务的时候,虽然可以保证新的服务映射会保存在子Context上,由于两个Context共享同一个Fiber,之前存储在store字典中的服务自然被覆盖了。
4. 是什么导致从Fiber中提取服务
到目前为止,一切都是我的猜测,如何确定注册的插件执行的时候会从Fiber中提取服务呢?由于插件仅仅是调用ctx.foobar()方法提取并执行服务函数,而且我们知道这个插件的ctx只是一个代理而已,针对foobar方法的读取会被代理处理器拦截,既然ctx的foobar属性返回了错误的对象,我们就通过代理处理器的定义来看看它究竟注入了怎样的拦截操作。通过DeepSeek Harness插件内核-07:Context全面解析的介绍,我们知道这个代理处理器定义在ReflectService的静态属性handler上。
exportclassReflectService{statichandler:ProxyHandler<Context>={get:(target,prop,ctx:Context)=>{...try{...returnctx.events.waterfall('internal/get',ctx,prop,error,()=>{constkey=target[symbols.isolate][prop]letfiber=(ctx[symbols.shadow]asContext??ctx).fiberwhile(true){constimpl=fiber.store?.[prop]if(impl)returngetTraceable(ctx,impl.value)if(propinfiber.inject){error.message=`cannot get required service "${prop}" in inactive context`throwerror}if(!fiber.runtime)throwerrorif(fiber.parent[symbols.isolate][prop]!==key)throwerror fiber=fiber.parent.fiber}})}catch(e:any){throwe===error?enhanceError(e):e}},}}ReflectService的静态属性handler返回一个ProxyHandler<Context>对象,上面提供了该对象get方法的部分代码,我们上面的假设在这里得到了印证(const impl = fiber.store?.[prop];if (impl) return getTraceable(ctx, impl.value))。