* add script manager & actuator * Otto package replaces Goja package * Resolve the issues raised by Copilot * add test for vm-pool * update GetServiceInvoker methodpull/651/merge
| @@ -1,6 +1,6 @@ | |||||
| module github.com/seata/seata-go | module github.com/seata/seata-go | ||||
| go 1.18 | |||||
| go 1.20 | |||||
| require ( | require ( | ||||
| dubbo.apache.org/dubbo-go/v3 v3.0.4 | dubbo.apache.org/dubbo-go/v3 v3.0.4 | ||||
| @@ -35,7 +35,8 @@ require ( | |||||
| github.com/agiledragon/gomonkey/v2 v2.9.0 | github.com/agiledragon/gomonkey/v2 v2.9.0 | ||||
| github.com/google/cel-go v0.18.0 | github.com/google/cel-go v0.18.0 | ||||
| github.com/mattn/go-sqlite3 v1.14.19 | github.com/mattn/go-sqlite3 v1.14.19 | ||||
| golang.org/x/sync v0.6.0 | |||||
| github.com/robertkrimen/otto v0.4.0 | |||||
| golang.org/x/sync v0.16.0 | |||||
| google.golang.org/protobuf v1.33.0 | google.golang.org/protobuf v1.33.0 | ||||
| gopkg.in/yaml.v3 v3.0.1 | gopkg.in/yaml.v3 v3.0.1 | ||||
| ) | ) | ||||
| @@ -90,8 +91,9 @@ require ( | |||||
| github.com/yusufpapurcu/wmi v1.2.2 // indirect | github.com/yusufpapurcu/wmi v1.2.2 // indirect | ||||
| go.uber.org/multierr v1.8.0 // indirect | go.uber.org/multierr v1.8.0 // indirect | ||||
| golang.org/x/arch v0.3.0 // indirect | golang.org/x/arch v0.3.0 // indirect | ||||
| golang.org/x/text v0.14.0 // indirect | |||||
| golang.org/x/text v0.27.0 // indirect | |||||
| gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect | gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect | ||||
| gopkg.in/sourcemap.v1 v1.0.5 // indirect | |||||
| ) | ) | ||||
| require ( | require ( | ||||
| @@ -106,7 +108,7 @@ require ( | |||||
| golang.org/x/crypto v0.17.0 // indirect | golang.org/x/crypto v0.17.0 // indirect | ||||
| golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect | golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 // indirect | ||||
| golang.org/x/net v0.10.0 // indirect | golang.org/x/net v0.10.0 // indirect | ||||
| golang.org/x/sys v0.15.0 // indirect | |||||
| golang.org/x/sys v0.32.0 // indirect | |||||
| google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect | google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect | ||||
| google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect | google.golang.org/genproto/googleapis/rpc v0.0.0-20230803162519-f966b187b2e5 // indirect | ||||
| vimagination.zapto.org/memio v0.0.0-20200222190306-588ebc67b97d // indirect | vimagination.zapto.org/memio v0.0.0-20200222190306-588ebc67b97d // indirect | ||||
| @@ -672,6 +672,8 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T | |||||
| github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= | ||||
| github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= | ||||
| github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= | github.com/rhnvrm/simples3 v0.6.1/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= | ||||
| github.com/robertkrimen/otto v0.4.0 h1:/c0GRrK1XDPcgIasAsnlpBT5DelIeB9U/Z/JCQsgr7E= | |||||
| github.com/robertkrimen/otto v0.4.0/go.mod h1:uW9yN1CYflmUQYvAMS0m+ZiNo3dMzRUDQJX0jWbzgxw= | |||||
| github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= | ||||
| github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= | ||||
| github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= | ||||
| @@ -952,8 +954,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ | |||||
| golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | ||||
| golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= | |||||
| golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= | |||||
| golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= | |||||
| golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= | |||||
| golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
| @@ -1031,8 +1033,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc | |||||
| golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= | |||||
| golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | |||||
| golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= | |||||
| golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= | |||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| @@ -1045,8 +1047,8 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |||||
| golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= | |||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | |||||
| golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= | |||||
| golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= | |||||
| golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= | ||||
| @@ -1251,6 +1253,8 @@ gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | |||||
| gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= | gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= | ||||
| gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= | gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= | ||||
| gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= | ||||
| gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI= | |||||
| gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78= | |||||
| gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= | gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= | ||||
| gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= | ||||
| gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= | ||||
| @@ -292,8 +292,17 @@ func (c *DefaultStateMachineConfig) GetExpressionFactory(expressionType string) | |||||
| return c.expressionFactoryManager.GetExpressionFactory(expressionType) | return c.expressionFactoryManager.GetExpressionFactory(expressionType) | ||||
| } | } | ||||
| func (c *DefaultStateMachineConfig) GetServiceInvoker(serviceType string) invoker.ServiceInvoker { | |||||
| return c.serviceInvokerManager.ServiceInvoker(serviceType) | |||||
| func (c *DefaultStateMachineConfig) GetServiceInvoker(serviceType string) (invoker.ServiceInvoker, error) { | |||||
| if serviceType == "" { | |||||
| serviceType = "local" | |||||
| } | |||||
| invoker := c.serviceInvokerManager.ServiceInvoker(serviceType) | |||||
| if invoker == nil { | |||||
| return nil, fmt.Errorf("service invoker not found for type: %s", serviceType) | |||||
| } | |||||
| return invoker, nil | |||||
| } | } | ||||
| func (c *DefaultStateMachineConfig) RegisterStateMachineDef(resources []string) error { | func (c *DefaultStateMachineConfig) RegisterStateMachineDef(resources []string) error { | ||||
| @@ -492,9 +501,20 @@ func (c *DefaultStateMachineConfig) initServiceInvokers() error { | |||||
| c.serviceInvokerManager = invoker.NewServiceInvokerManagerImpl() | c.serviceInvokerManager = invoker.NewServiceInvokerManagerImpl() | ||||
| } | } | ||||
| defaultServiceType := "local" | |||||
| if existingInvoker := c.serviceInvokerManager.ServiceInvoker(defaultServiceType); existingInvoker == nil { | |||||
| c.RegisterServiceInvoker(defaultServiceType, invoker.NewLocalServiceInvoker()) | |||||
| if existing := c.serviceInvokerManager.ServiceInvoker("local"); existing == nil { | |||||
| c.RegisterServiceInvoker("local", invoker.NewLocalServiceInvoker()) | |||||
| } | |||||
| if existing := c.serviceInvokerManager.ServiceInvoker("http"); existing == nil { | |||||
| c.RegisterServiceInvoker("http", invoker.NewHTTPInvoker()) | |||||
| } | |||||
| if existing := c.serviceInvokerManager.ServiceInvoker("grpc"); existing == nil { | |||||
| c.RegisterServiceInvoker("grpc", invoker.NewGRPCInvoker()) | |||||
| } | |||||
| if existing := c.serviceInvokerManager.ServiceInvoker("func"); existing == nil { | |||||
| c.RegisterServiceInvoker("func", invoker.NewFuncInvoker()) | |||||
| } | } | ||||
| return nil | return nil | ||||
| @@ -20,7 +20,6 @@ package invoker | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "encoding/json" | "encoding/json" | ||||
| "fmt" | |||||
| "reflect" | "reflect" | ||||
| "sync" | "sync" | ||||
| @@ -43,164 +42,72 @@ func (p *DefaultJsonParser) Marshal(v any) ([]byte, error) { | |||||
| } | } | ||||
| type ScriptInvokerManager interface { | type ScriptInvokerManager interface { | ||||
| GetInvoker(scriptType string) (ScriptInvoker, error) | |||||
| RegisterInvoker(invoker ScriptInvoker) | |||||
| Execute(ctx context.Context, scriptType string, script string, params map[string]interface{}) (interface{}, error) | |||||
| } | } | ||||
| type ScriptInvoker interface { | type ScriptInvoker interface { | ||||
| } | |||||
| type ServiceInvokerManager interface { | |||||
| ServiceInvoker(serviceType string) ServiceInvoker | |||||
| PutServiceInvoker(serviceType string, invoker ServiceInvoker) | |||||
| } | |||||
| type ServiceInvoker interface { | |||||
| Invoke(ctx context.Context, input []any, service state.ServiceTaskState) (output []reflect.Value, err error) | |||||
| Invoke(ctx context.Context, script string, params map[string]interface{}) (interface{}, error) | |||||
| Type() string | |||||
| Close(ctx context.Context) error | Close(ctx context.Context) error | ||||
| } | } | ||||
| type ServiceInvokerManagerImpl struct { | |||||
| invokers map[string]ServiceInvoker | |||||
| type ScriptInvokerManagerImpl struct { | |||||
| invokers map[string]ScriptInvoker | |||||
| mutex sync.Mutex | mutex sync.Mutex | ||||
| } | } | ||||
| type LocalServiceInvoker struct { | |||||
| serviceRegistry map[string]interface{} | |||||
| methodCache map[string]*reflect.Method | |||||
| jsonParser JsonParser | |||||
| mutex sync.RWMutex | |||||
| } | |||||
| func NewLocalServiceInvoker() *LocalServiceInvoker { | |||||
| return &LocalServiceInvoker{ | |||||
| serviceRegistry: make(map[string]interface{}), | |||||
| methodCache: make(map[string]*reflect.Method), | |||||
| jsonParser: &DefaultJsonParser{}, | |||||
| } | |||||
| } | |||||
| func (l *LocalServiceInvoker) RegisterService(serviceName string, instance interface{}) { | |||||
| l.mutex.Lock() | |||||
| defer l.mutex.Unlock() | |||||
| l.serviceRegistry[serviceName] = instance | |||||
| } | |||||
| func (l *LocalServiceInvoker) Invoke(ctx context.Context, input []any, service state.ServiceTaskState) ([]reflect.Value, error) { | |||||
| serviceName := service.ServiceName() | |||||
| instance, exists := l.serviceRegistry[serviceName] | |||||
| if !exists { | |||||
| return nil, fmt.Errorf("service %s not registered", serviceName) | |||||
| } | |||||
| methodName := service.ServiceMethod() | |||||
| method, err := l.getMethod(serviceName, methodName, service.ParameterTypes()) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| params, err := l.resolveParameters(input, method.Type) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| func NewScriptInvokerManager() *ScriptInvokerManagerImpl { | |||||
| return &ScriptInvokerManagerImpl{ | |||||
| invokers: make(map[string]ScriptInvoker), | |||||
| } | } | ||||
| return l.invokeMethod(instance, method, params), nil | |||||
| } | } | ||||
| func (l *LocalServiceInvoker) resolveMethod(key, serviceName, methodName string) (*reflect.Method, error) { | |||||
| l.mutex.Lock() | |||||
| defer l.mutex.Unlock() | |||||
| if cachedMethod, ok := l.methodCache[key]; ok { | |||||
| return cachedMethod, nil | |||||
| func (m *ScriptInvokerManagerImpl) GetInvoker(scriptType string) (ScriptInvoker, error) { | |||||
| if scriptType == "" { | |||||
| return nil, nil | |||||
| } | } | ||||
| m.mutex.Lock() | |||||
| defer m.mutex.Unlock() | |||||
| instance, exists := l.serviceRegistry[serviceName] | |||||
| invoker, exists := m.invokers[scriptType] | |||||
| if !exists { | if !exists { | ||||
| return nil, fmt.Errorf("service %s not found", serviceName) | |||||
| } | |||||
| objType := reflect.TypeOf(instance) | |||||
| method, ok := objType.MethodByName(methodName) | |||||
| if !ok { | |||||
| return nil, fmt.Errorf("method %s not found in service %s", methodName, serviceName) | |||||
| return nil, nil | |||||
| } | } | ||||
| l.methodCache[key] = &method | |||||
| return &method, nil | |||||
| return invoker, nil | |||||
| } | } | ||||
| func (l *LocalServiceInvoker) getMethod(serviceName, methodName string, paramTypes []string) (*reflect.Method, error) { | |||||
| key := fmt.Sprintf("%s.%s", serviceName, methodName) | |||||
| l.mutex.RLock() | |||||
| if method, ok := l.methodCache[key]; ok { | |||||
| l.mutex.RUnlock() | |||||
| return method, nil | |||||
| func (m *ScriptInvokerManagerImpl) RegisterInvoker(invoker ScriptInvoker) { | |||||
| if invoker == nil || invoker.Type() == "" { | |||||
| return | |||||
| } | } | ||||
| l.mutex.RUnlock() | |||||
| return l.resolveMethod(key, serviceName, methodName) | |||||
| m.mutex.Lock() | |||||
| defer m.mutex.Unlock() | |||||
| m.invokers[invoker.Type()] = invoker | |||||
| } | } | ||||
| func (l *LocalServiceInvoker) resolveParameters(input []any, methodType reflect.Type) ([]reflect.Value, error) { | |||||
| params := make([]reflect.Value, methodType.NumIn()) | |||||
| for i := 0; i < methodType.NumIn(); i++ { | |||||
| paramType := methodType.In(i) | |||||
| if i >= len(input) { | |||||
| params[i] = reflect.Zero(paramType) | |||||
| continue | |||||
| } | |||||
| converted, err := l.convertParam(input[i], paramType) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| params[i] = reflect.ValueOf(converted) | |||||
| func (m *ScriptInvokerManagerImpl) Execute(ctx context.Context, scriptType string, script string, params map[string]interface{}) (interface{}, error) { | |||||
| invoker, err := m.GetInvoker(scriptType) | |||||
| if err != nil || invoker == nil { | |||||
| return nil, err | |||||
| } | } | ||||
| return params, nil | |||||
| return invoker.Invoke(ctx, script, params) | |||||
| } | } | ||||
| func (l *LocalServiceInvoker) convertParam(value any, targetType reflect.Type) (any, error) { | |||||
| if targetType.Kind() == reflect.Ptr { | |||||
| targetType = targetType.Elem() | |||||
| value = reflect.ValueOf(value).Interface() | |||||
| } | |||||
| if targetType.Kind() == reflect.Int && reflect.TypeOf(value).Kind() == reflect.Float64 { | |||||
| return int(value.(float64)), nil | |||||
| } else if targetType == reflect.TypeOf("") && reflect.TypeOf(value).Kind() == reflect.Int { | |||||
| return fmt.Sprintf("%d", value), nil | |||||
| } | |||||
| if targetType.Kind() == reflect.Struct { | |||||
| jsonData, err := l.jsonParser.Marshal(value) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| instance := reflect.New(targetType).Interface() | |||||
| if err := l.jsonParser.Unmarshal(jsonData, instance); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return instance, nil | |||||
| } | |||||
| return value, nil | |||||
| type ServiceInvokerManager interface { | |||||
| ServiceInvoker(serviceType string) ServiceInvoker | |||||
| PutServiceInvoker(serviceType string, invoker ServiceInvoker) | |||||
| } | } | ||||
| func (l *LocalServiceInvoker) invokeMethod(instance interface{}, method *reflect.Method, params []reflect.Value) []reflect.Value { | |||||
| instanceValue := reflect.ValueOf(instance) | |||||
| if method.Func.IsValid() { | |||||
| allParams := append([]reflect.Value{instanceValue}, params...) | |||||
| return method.Func.Call(allParams) | |||||
| } | |||||
| return nil | |||||
| type ServiceInvoker interface { | |||||
| Invoke(ctx context.Context, input []any, service state.ServiceTaskState) (output []reflect.Value, err error) | |||||
| Close(ctx context.Context) error | |||||
| } | } | ||||
| func (l *LocalServiceInvoker) Close(ctx context.Context) error { | |||||
| l.mutex.Lock() | |||||
| defer l.mutex.Unlock() | |||||
| l.serviceRegistry = nil | |||||
| l.methodCache = nil | |||||
| return nil | |||||
| type ServiceInvokerManagerImpl struct { | |||||
| invokers map[string]ServiceInvoker | |||||
| mutex sync.Mutex | |||||
| } | } | ||||
| func NewServiceInvokerManagerImpl() *ServiceInvokerManagerImpl { | func NewServiceInvokerManagerImpl() *ServiceInvokerManagerImpl { | ||||
| @@ -0,0 +1,162 @@ | |||||
| /* | |||||
| * 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 invoker | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "sync" | |||||
| "github.com/robertkrimen/otto" | |||||
| ) | |||||
| const defaultPoolSize = 10 | |||||
| type JavaScriptScriptInvoker struct { | |||||
| mutex sync.Mutex | |||||
| jsonParser JsonParser | |||||
| closed bool | |||||
| vmPool chan *otto.Otto | |||||
| poolSize int | |||||
| } | |||||
| func NewJavaScriptScriptInvoker() *JavaScriptScriptInvoker { | |||||
| return &JavaScriptScriptInvoker{ | |||||
| jsonParser: &DefaultJsonParser{}, | |||||
| closed: false, | |||||
| poolSize: defaultPoolSize, | |||||
| vmPool: make(chan *otto.Otto, defaultPoolSize), | |||||
| } | |||||
| } | |||||
| func NewJavaScriptScriptInvokerWithPoolSize(poolSize int) *JavaScriptScriptInvoker { | |||||
| if poolSize <= 0 { | |||||
| poolSize = defaultPoolSize | |||||
| } | |||||
| return &JavaScriptScriptInvoker{ | |||||
| jsonParser: &DefaultJsonParser{}, | |||||
| closed: false, | |||||
| poolSize: poolSize, | |||||
| vmPool: make(chan *otto.Otto, poolSize), | |||||
| } | |||||
| } | |||||
| func (j *JavaScriptScriptInvoker) Type() string { | |||||
| return "javascript" | |||||
| } | |||||
| func (j *JavaScriptScriptInvoker) Invoke(ctx context.Context, script string, params map[string]interface{}) (interface{}, error) { | |||||
| j.mutex.Lock() | |||||
| closed := j.closed | |||||
| j.mutex.Unlock() | |||||
| if closed { | |||||
| return nil, fmt.Errorf("javascript invoker has been closed") | |||||
| } | |||||
| var vm *otto.Otto | |||||
| select { | |||||
| case vm = <-j.vmPool: | |||||
| if err := cleanVMState(vm); err != nil { | |||||
| vm = otto.New() | |||||
| } | |||||
| default: | |||||
| vm = otto.New() | |||||
| } | |||||
| defer func() { | |||||
| j.mutex.Lock() | |||||
| defer j.mutex.Unlock() | |||||
| if !j.closed { | |||||
| select { | |||||
| case j.vmPool <- vm: | |||||
| default: | |||||
| // Pool full, discard current instance | |||||
| } | |||||
| } | |||||
| }() | |||||
| for key, value := range params { | |||||
| if err := vm.Set(key, value); err != nil { | |||||
| return nil, fmt.Errorf("javascript set param %s error: %w", key, err) | |||||
| } | |||||
| } | |||||
| resultChan := make(chan struct { | |||||
| val otto.Value | |||||
| err error | |||||
| }, 1) | |||||
| go func() { | |||||
| defer func() { | |||||
| if r := recover(); r != nil { | |||||
| resultChan <- struct { | |||||
| val otto.Value | |||||
| err error | |||||
| }{otto.UndefinedValue(), fmt.Errorf("javascript engine panic: %v", r)} | |||||
| } | |||||
| }() | |||||
| val, err := vm.Run(script) | |||||
| resultChan <- struct { | |||||
| val otto.Value | |||||
| err error | |||||
| }{val, err} | |||||
| }() | |||||
| select { | |||||
| case <-ctx.Done(): | |||||
| return nil, fmt.Errorf("javascript execution timeout: %w", ctx.Err()) | |||||
| case res := <-resultChan: | |||||
| if res.err != nil { | |||||
| return nil, fmt.Errorf("javascript execute error: %w", res.err) | |||||
| } | |||||
| val, err := res.val.Export() | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("failed to export javascript result: %w", err) | |||||
| } | |||||
| return val, nil | |||||
| } | |||||
| } | |||||
| func (j *JavaScriptScriptInvoker) Close(ctx context.Context) error { | |||||
| j.mutex.Lock() | |||||
| defer j.mutex.Unlock() | |||||
| if j.closed { | |||||
| return nil | |||||
| } | |||||
| j.closed = true | |||||
| close(j.vmPool) | |||||
| for range j.vmPool { | |||||
| // Let GC recycle VM resources | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func cleanVMState(vm *otto.Otto) error { | |||||
| _, err := vm.Run(` | |||||
| for (const prop in global) { | |||||
| if (!['Object', 'Array', 'Function', 'String', 'Number', 'Boolean', 'JSON', 'Date', 'RegExp'].includes(prop)) { | |||||
| delete global[prop]; | |||||
| } | |||||
| } | |||||
| `) | |||||
| return err | |||||
| } | |||||
| @@ -0,0 +1,262 @@ | |||||
| /* | |||||
| * 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 invoker | |||||
| import ( | |||||
| "context" | |||||
| "sync" | |||||
| "testing" | |||||
| "time" | |||||
| "github.com/robertkrimen/otto" | |||||
| "github.com/stretchr/testify/assert" | |||||
| ) | |||||
| func TestJavaScriptScriptInvoker_Type(t *testing.T) { | |||||
| invoker := NewJavaScriptScriptInvoker() | |||||
| assert.Equal(t, "javascript", invoker.Type()) | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_Invoke_Basic(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| script string | |||||
| params map[string]interface{} | |||||
| expected interface{} | |||||
| }{ | |||||
| { | |||||
| name: "simple expression", | |||||
| script: "1 + 2", | |||||
| params: nil, | |||||
| expected: float64(3), | |||||
| }, | |||||
| { | |||||
| name: "param calculation", | |||||
| script: "a * b + c", | |||||
| params: map[string]interface{}{"a": 2, "b": 3, "c": 4}, | |||||
| expected: float64(10), | |||||
| }, | |||||
| { | |||||
| name: "return string", | |||||
| script: "['hello', name].join(' ')", | |||||
| params: map[string]interface{}{"name": "world"}, | |||||
| expected: "hello world", | |||||
| }, | |||||
| { | |||||
| name: "return object", | |||||
| script: `var obj = {id: 1, name: name}; obj;`, | |||||
| params: map[string]interface{}{"name": "test"}, | |||||
| expected: map[string]interface{}{"id": float64(1), "name": "test"}, | |||||
| }, | |||||
| } | |||||
| invoker := NewJavaScriptScriptInvoker() | |||||
| ctx := context.Background() | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| result, err := invoker.Invoke(ctx, tt.script, tt.params) | |||||
| assert.NoError(t, err) | |||||
| if resultMap, ok := result.(map[string]interface{}); ok { | |||||
| for k, v := range resultMap { | |||||
| if intVal, isInt := v.(int64); isInt { | |||||
| resultMap[k] = float64(intVal) | |||||
| } | |||||
| } | |||||
| } | |||||
| assert.Equal(t, tt.expected, result) | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_Invoke_Error(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| script string | |||||
| params map[string]interface{} | |||||
| errMsg string | |||||
| }{ | |||||
| { | |||||
| name: "syntax error", | |||||
| script: "1 + ", | |||||
| params: nil, | |||||
| errMsg: "javascript execute error", | |||||
| }, | |||||
| { | |||||
| name: "reference undefined variable", | |||||
| script: "undefinedVar", | |||||
| params: nil, | |||||
| errMsg: "javascript execute error", | |||||
| }, | |||||
| } | |||||
| invoker := NewJavaScriptScriptInvoker() | |||||
| ctx := context.Background() | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| _, err := invoker.Invoke(ctx, tt.script, tt.params) | |||||
| if err == nil { | |||||
| t.Fatalf("Test case [%s] expected error but got none", tt.name) | |||||
| } | |||||
| assert.Contains(t, err.Error(), tt.errMsg, "Test case [%s] error message mismatch", tt.name) | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_Invoke_Timeout(t *testing.T) { | |||||
| script := `var target = 300; var start = new Date().getTime(); var elapsed = 0; while (elapsed < target) { elapsed = new Date().getTime() - start; } "done";` | |||||
| invoker := NewJavaScriptScriptInvoker() | |||||
| ctx1, cancel1 := context.WithTimeout(context.Background(), 200*time.Millisecond) | |||||
| defer cancel1() | |||||
| _, err := invoker.Invoke(ctx1, script, nil) | |||||
| assert.Error(t, err) | |||||
| assert.Contains(t, err.Error(), "javascript execution timeout") | |||||
| ctx2, cancel2 := context.WithTimeout(context.Background(), 400*time.Millisecond) | |||||
| defer cancel2() | |||||
| result, err := invoker.Invoke(ctx2, script, nil) | |||||
| assert.NoError(t, err, "Scenario 2: script execution should not return error") | |||||
| assert.Equal(t, "done", result, "Scenario 2: should return 'done'") | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_Invoke_Concurrent(t *testing.T) { | |||||
| invoker := NewJavaScriptScriptInvoker() | |||||
| ctx := context.Background() | |||||
| var wg sync.WaitGroup | |||||
| concurrency := 100 | |||||
| errChan := make(chan error, concurrency) | |||||
| script := `a + b` | |||||
| params := map[string]interface{}{"a": 10, "b": 20} | |||||
| for i := 0; i < concurrency; i++ { | |||||
| wg.Add(1) | |||||
| go func() { | |||||
| defer wg.Done() | |||||
| result, err := invoker.Invoke(ctx, script, params) | |||||
| if err != nil { | |||||
| errChan <- err | |||||
| return | |||||
| } | |||||
| if result != float64(30) { | |||||
| errChan <- assert.AnError | |||||
| } | |||||
| }() | |||||
| } | |||||
| wg.Wait() | |||||
| close(errChan) | |||||
| assert.Empty(t, errChan, "Concurrent execution has errors") | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_Close(t *testing.T) { | |||||
| invoker := NewJavaScriptScriptInvoker() | |||||
| ctx := context.Background() | |||||
| result, err := invoker.Invoke(ctx, "1 + 1", nil) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, float64(2), result) | |||||
| err = invoker.Close(ctx) | |||||
| assert.NoError(t, err) | |||||
| _, err = invoker.Invoke(ctx, "1 + 1", nil) | |||||
| assert.Error(t, err) | |||||
| assert.Contains(t, err.Error(), "javascript invoker has been closed") | |||||
| } | |||||
| func TestOttoScript(t *testing.T) { | |||||
| vm := otto.New() | |||||
| script := `var target = 300; var start = new Date().getTime(); var elapsed = 0; while (elapsed < target) { elapsed = new Date().getTime() - start; } "done";` | |||||
| val, err := vm.Run(script) | |||||
| if err != nil { | |||||
| t.Fatalf("otto failed to parse script: %v", err) | |||||
| } | |||||
| result, exportErr := val.Export() | |||||
| if exportErr != nil { | |||||
| t.Fatalf("failed to export otto value: %v", exportErr) | |||||
| } | |||||
| t.Logf("Script execution result: %v", result) | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_VMPoolReuse(t *testing.T) { | |||||
| poolSize := 2 | |||||
| invoker := NewJavaScriptScriptInvokerWithPoolSize(poolSize) | |||||
| ctx := context.Background() | |||||
| vmIDs := make([]string, 0, 5) | |||||
| script := ` | |||||
| if (!this.vmId) { | |||||
| this.vmId = Math.random().toString(36).substr(2, 8); | |||||
| } | |||||
| this.vmId; | |||||
| ` | |||||
| for i := 0; i < 5; i++ { | |||||
| result, err := invoker.Invoke(ctx, script, nil) | |||||
| assert.NoError(t, err, "Error occurred while executing script") | |||||
| id, ok := result.(string) | |||||
| assert.True(t, ok, "VM ID should be a string type") | |||||
| vmIDs = append(vmIDs, id) | |||||
| } | |||||
| uniqueIDs := make(map[string]bool) | |||||
| for _, id := range vmIDs { | |||||
| uniqueIDs[id] = true | |||||
| } | |||||
| assert.True(t, len(uniqueIDs) <= 5, "Abnormal number of VM instances created") | |||||
| assert.True(t, len(uniqueIDs) >= 1, "No VM instances reused from the pool") | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_VMStateClean(t *testing.T) { | |||||
| invoker := NewJavaScriptScriptInvokerWithPoolSize(1) | |||||
| ctx := context.Background() | |||||
| _, err := invoker.Invoke(ctx, `this.foo = "polluted data"`, nil) | |||||
| assert.NoError(t, err) | |||||
| result, err := invoker.Invoke(ctx, `typeof this.foo`, nil) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, "undefined", result, "VM state not cleaned, residual global variable exists") | |||||
| _, err = invoker.Invoke(ctx, `this.bar = function() { return "residual function"; }`, nil) | |||||
| assert.NoError(t, err) | |||||
| result, err = invoker.Invoke(ctx, `typeof this.bar`, nil) | |||||
| assert.NoError(t, err) | |||||
| assert.Equal(t, "undefined", result, "VM state not cleaned, residual function exists") | |||||
| } | |||||
| func TestJavaScriptScriptInvoker_PoolSizeDefault(t *testing.T) { | |||||
| invoker := NewJavaScriptScriptInvokerWithPoolSize(0) | |||||
| assert.Equal(t, defaultPoolSize, invoker.poolSize, "Default pool size not used when pool size is 0") | |||||
| invoker = NewJavaScriptScriptInvokerWithPoolSize(-5) | |||||
| assert.Equal(t, defaultPoolSize, invoker.poolSize, "Default pool size not used when pool size is negative") | |||||
| } | |||||
| @@ -0,0 +1,195 @@ | |||||
| /* | |||||
| * 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 invoker | |||||
| import ( | |||||
| "context" | |||||
| "fmt" | |||||
| "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" | |||||
| "reflect" | |||||
| "sync" | |||||
| ) | |||||
| type LocalServiceInvoker struct { | |||||
| serviceRegistry map[string]interface{} | |||||
| methodCache map[string]*reflect.Method | |||||
| jsonParser JsonParser | |||||
| mutex sync.RWMutex | |||||
| } | |||||
| func NewLocalServiceInvoker() *LocalServiceInvoker { | |||||
| return &LocalServiceInvoker{ | |||||
| serviceRegistry: make(map[string]interface{}), | |||||
| methodCache: make(map[string]*reflect.Method), | |||||
| jsonParser: &DefaultJsonParser{}, | |||||
| } | |||||
| } | |||||
| func (l *LocalServiceInvoker) RegisterService(serviceName string, instance interface{}) { | |||||
| l.mutex.Lock() | |||||
| defer l.mutex.Unlock() | |||||
| l.serviceRegistry[serviceName] = instance | |||||
| } | |||||
| func (l *LocalServiceInvoker) Invoke(ctx context.Context, input []any, service state.ServiceTaskState) ([]reflect.Value, error) { | |||||
| serviceName := service.ServiceName() | |||||
| instance, exists := l.serviceRegistry[serviceName] | |||||
| if !exists { | |||||
| return nil, fmt.Errorf("service %s not registered", serviceName) | |||||
| } | |||||
| methodName := service.ServiceMethod() | |||||
| method, err := l.getMethod(serviceName, methodName, service.ParameterTypes()) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| params, err := l.resolveParameters(input, method.Type) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return l.invokeMethod(instance, method, params), nil | |||||
| } | |||||
| func (l *LocalServiceInvoker) resolveMethod(key, serviceName, methodName string) (*reflect.Method, error) { | |||||
| l.mutex.Lock() | |||||
| defer l.mutex.Unlock() | |||||
| if cachedMethod, ok := l.methodCache[key]; ok { | |||||
| return cachedMethod, nil | |||||
| } | |||||
| instance, exists := l.serviceRegistry[serviceName] | |||||
| if !exists { | |||||
| return nil, fmt.Errorf("service %s not found", serviceName) | |||||
| } | |||||
| objType := reflect.TypeOf(instance) | |||||
| method, ok := objType.MethodByName(methodName) | |||||
| if !ok { | |||||
| return nil, fmt.Errorf("method %s not found in service %s", methodName, serviceName) | |||||
| } | |||||
| l.methodCache[key] = &method | |||||
| return &method, nil | |||||
| } | |||||
| func (l *LocalServiceInvoker) getMethod(serviceName, methodName string, paramTypes []string) (*reflect.Method, error) { | |||||
| key := fmt.Sprintf("%s.%s", serviceName, methodName) | |||||
| l.mutex.RLock() | |||||
| if method, ok := l.methodCache[key]; ok { | |||||
| l.mutex.RUnlock() | |||||
| return method, nil | |||||
| } | |||||
| l.mutex.RUnlock() | |||||
| return l.resolveMethod(key, serviceName, methodName) | |||||
| } | |||||
| func (l *LocalServiceInvoker) resolveParameters(input []any, methodType reflect.Type) ([]reflect.Value, error) { | |||||
| numIn := methodType.NumIn() | |||||
| paramStart, paramCount := 1, 0 | |||||
| if numIn > 0 { | |||||
| paramCount = numIn - paramStart | |||||
| } | |||||
| if paramCount == 0 { | |||||
| if len(input) > 0 { | |||||
| return nil, fmt.Errorf("unexpected parameters: expected 0, got %d", len(input)) | |||||
| } | |||||
| return []reflect.Value{}, nil | |||||
| } | |||||
| if len(input) < paramCount { | |||||
| return nil, fmt.Errorf("insufficient parameters: expected %d, got %d", paramCount, len(input)) | |||||
| } | |||||
| if len(input) > paramCount { | |||||
| return nil, fmt.Errorf("too many parameters: expected %d, got %d", paramCount, len(input)) | |||||
| } | |||||
| params := make([]reflect.Value, paramCount) | |||||
| for i := 0; i < paramCount; i++ { | |||||
| methodParamIndex := i + paramStart | |||||
| paramType := methodType.In(methodParamIndex) | |||||
| converted, err := l.convertParam(input[i], paramType) | |||||
| if err != nil { | |||||
| return nil, fmt.Errorf("parameter %d conversion error: %w", i, err) | |||||
| } | |||||
| params[i] = reflect.ValueOf(converted) | |||||
| } | |||||
| return params, nil | |||||
| } | |||||
| func (l *LocalServiceInvoker) convertParam(value any, targetType reflect.Type) (any, error) { | |||||
| if targetType.Kind() == reflect.Ptr { | |||||
| elemType := targetType.Elem() | |||||
| instance := reflect.New(elemType).Interface() | |||||
| jsonData, err := l.jsonParser.Marshal(value) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if err := l.jsonParser.Unmarshal(jsonData, instance); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return instance, nil | |||||
| } | |||||
| if targetType.Kind() == reflect.Struct { | |||||
| instance := reflect.New(targetType).Interface() | |||||
| jsonData, err := l.jsonParser.Marshal(value) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| if err := l.jsonParser.Unmarshal(jsonData, instance); err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return reflect.ValueOf(instance).Elem().Interface(), nil | |||||
| } | |||||
| if targetType.Kind() == reflect.Int && reflect.TypeOf(value).Kind() == reflect.Float64 { | |||||
| return int(value.(float64)), nil | |||||
| } else if targetType == reflect.TypeOf("") && reflect.TypeOf(value).Kind() == reflect.Int { | |||||
| return fmt.Sprintf("%d", value), nil | |||||
| } | |||||
| return value, nil | |||||
| } | |||||
| func (l *LocalServiceInvoker) invokeMethod(instance interface{}, method *reflect.Method, params []reflect.Value) []reflect.Value { | |||||
| instanceValue := reflect.ValueOf(instance) | |||||
| if method.Func.IsValid() { | |||||
| allParams := append([]reflect.Value{instanceValue}, params...) | |||||
| return method.Func.Call(allParams) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (l *LocalServiceInvoker) Close(ctx context.Context) error { | |||||
| l.mutex.Lock() | |||||
| defer l.mutex.Unlock() | |||||
| l.serviceRegistry = nil | |||||
| l.methodCache = nil | |||||
| return nil | |||||
| } | |||||
| @@ -0,0 +1,212 @@ | |||||
| /* | |||||
| * 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 invoker | |||||
| import ( | |||||
| "context" | |||||
| "errors" | |||||
| "fmt" | |||||
| "reflect" | |||||
| "testing" | |||||
| "github.com/seata/seata-go/pkg/saga/statemachine/statelang/state" | |||||
| ) | |||||
| type MockLocalService struct { | |||||
| invokeCount int | |||||
| } | |||||
| func (m *MockLocalService) GetServiceName() string { | |||||
| return "MockLocalService" | |||||
| } | |||||
| func (m *MockLocalService) Add(a, b int) int { | |||||
| m.invokeCount++ | |||||
| return a + b | |||||
| } | |||||
| func (m *MockLocalService) Multiply(f float64, i int) float64 { | |||||
| m.invokeCount++ | |||||
| return f * float64(i) | |||||
| } | |||||
| type User struct { | |||||
| Name string `json:"name"` | |||||
| Age int `json:"age"` | |||||
| } | |||||
| func (m *MockLocalService) GetUserName(user User) string { | |||||
| m.invokeCount++ | |||||
| return user.Name | |||||
| } | |||||
| func (m *MockLocalService) ErrorMethod() error { | |||||
| return errors.New("expected error") | |||||
| } | |||||
| func TestLocalInvoker_ServiceNotRegistered(t *testing.T) { | |||||
| invoker := NewLocalServiceInvoker() | |||||
| ctx := context.Background() | |||||
| taskState := newLocalServiceTaskState("unregisteredService", "AnyMethod") | |||||
| _, err := invoker.Invoke(ctx, []any{}, taskState) | |||||
| if err == nil { | |||||
| t.Error("expected error when service not registered, but got nil") | |||||
| } | |||||
| if err.Error() != "service unregisteredService not registered" { | |||||
| t.Errorf("unexpected error message: %v", err) | |||||
| } | |||||
| } | |||||
| func TestLocalInvoker_MethodNotFound(t *testing.T) { | |||||
| invoker := NewLocalServiceInvoker() | |||||
| service := &MockLocalService{} | |||||
| invoker.RegisterService("mockService", service) | |||||
| ctx := context.Background() | |||||
| taskState := newLocalServiceTaskState("mockService", "NonExistentMethod") | |||||
| _, err := invoker.Invoke(ctx, []any{}, taskState) | |||||
| if err == nil { | |||||
| t.Error("expected error when method not found, but got nil") | |||||
| } | |||||
| if err.Error() != "method NonExistentMethod not found in service mockService" { | |||||
| t.Errorf("unexpected error message: %v", err) | |||||
| } | |||||
| } | |||||
| func TestLocalInvoker_InvokeSuccess(t *testing.T) { | |||||
| tests := []struct { | |||||
| name string | |||||
| service interface{} | |||||
| serviceName string | |||||
| methodName string | |||||
| input []any | |||||
| expected interface{} | |||||
| }{ | |||||
| { | |||||
| name: "test basic method call", | |||||
| service: &MockLocalService{}, | |||||
| serviceName: "mockService", | |||||
| methodName: "GetServiceName", | |||||
| input: []any{}, | |||||
| expected: "MockLocalService", | |||||
| }, | |||||
| { | |||||
| name: "test method with parameters", | |||||
| service: &MockLocalService{}, | |||||
| serviceName: "mockService", | |||||
| methodName: "Add", | |||||
| input: []any{2, 3}, | |||||
| expected: 5, | |||||
| }, | |||||
| { | |||||
| name: "test parameter type conversion", | |||||
| service: &MockLocalService{}, | |||||
| serviceName: "mockService", | |||||
| methodName: "Multiply", | |||||
| input: []any{2.5, 4}, | |||||
| expected: 10.0, | |||||
| }, | |||||
| } | |||||
| invoker := NewLocalServiceInvoker() | |||||
| ctx := context.Background() | |||||
| for _, tt := range tests { | |||||
| t.Run(tt.name, func(t *testing.T) { | |||||
| invoker.RegisterService(tt.serviceName, tt.service) | |||||
| taskState := newLocalServiceTaskState(tt.serviceName, tt.methodName) | |||||
| results, err := invoker.Invoke(ctx, tt.input, taskState) | |||||
| if err != nil { | |||||
| t.Fatalf("unexpected error: %v", err) | |||||
| } | |||||
| if len(results) == 0 { | |||||
| t.Fatal("no results returned") | |||||
| } | |||||
| result := results[0].Interface() | |||||
| if !reflect.DeepEqual(result, tt.expected) { | |||||
| t.Errorf("expected %v, got %v", tt.expected, result) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| func TestLocalInvoker_StructParameterConversion(t *testing.T) { | |||||
| invoker := NewLocalServiceInvoker() | |||||
| service := &MockLocalService{} | |||||
| invoker.RegisterService("userService", service) | |||||
| ctx := context.Background() | |||||
| taskState := newLocalServiceTaskState("userService", "GetUserName") | |||||
| input := []any{map[string]interface{}{"name": "Alice", "age": 30}} | |||||
| results, err := invoker.Invoke(ctx, input, taskState) | |||||
| if err != nil { | |||||
| t.Fatalf("unexpected error: %v", err) | |||||
| } | |||||
| if len(results) == 0 { | |||||
| t.Fatal("no results returned") | |||||
| } | |||||
| result := results[0].Interface() | |||||
| if result != "Alice" { | |||||
| t.Errorf("expected 'Alice', got %v", result) | |||||
| } | |||||
| } | |||||
| func TestLocalInvoker_MethodCaching(t *testing.T) { | |||||
| invoker := NewLocalServiceInvoker() | |||||
| service := &MockLocalService{} | |||||
| invoker.RegisterService("cacheTestService", service) | |||||
| ctx := context.Background() | |||||
| taskState := newLocalServiceTaskState("cacheTestService", "Add") | |||||
| _, err := invoker.Invoke(ctx, []any{1, 1}, taskState) | |||||
| if err != nil { | |||||
| t.Fatalf("unexpected error: %v", err) | |||||
| } | |||||
| results, err := invoker.Invoke(ctx, []any{2, 3}, taskState) | |||||
| if err != nil { | |||||
| t.Fatalf("unexpected error: %v", err) | |||||
| } | |||||
| if results[0].Interface() != 5 { | |||||
| t.Errorf("expected 5, got %v", results[0].Interface()) | |||||
| } | |||||
| if service.invokeCount != 2 { | |||||
| t.Errorf("expected 2 invocations, got %d", service.invokeCount) | |||||
| } | |||||
| } | |||||
| func newLocalServiceTaskState(serviceName, methodName string) state.ServiceTaskState { | |||||
| serviceTaskStateImpl := state.NewServiceTaskStateImpl() | |||||
| serviceTaskStateImpl.SetName(fmt.Sprintf("%s_%s", serviceName, methodName)) | |||||
| serviceTaskStateImpl.SetIsAsync(false) | |||||
| serviceTaskStateImpl.SetServiceName(serviceName) | |||||
| serviceTaskStateImpl.SetServiceType("local") | |||||
| serviceTaskStateImpl.SetServiceMethod(methodName) | |||||
| return serviceTaskStateImpl | |||||
| } | |||||