| @@ -23,6 +23,7 @@ const ( | |||||
| type NodeEnv struct { | type NodeEnv struct { | ||||
| Type NodeEnvType | Type NodeEnvType | ||||
| Worker exec.WorkerInfo | Worker exec.WorkerInfo | ||||
| Pinned bool // 如果为true,则不应该改变这个节点的执行环境 | |||||
| } | } | ||||
| func (e *NodeEnv) ToEnvUnknown() { | func (e *NodeEnv) ToEnvUnknown() { | ||||
| @@ -84,7 +84,8 @@ func NodeDeclareInputStream(node *Node, cnt int) { | |||||
| type ValueVarType int | type ValueVarType int | ||||
| const ( | const ( | ||||
| StringValueVar ValueVarType = iota | |||||
| UnknownValueVar ValueVarType = iota | |||||
| StringValueVar | |||||
| SignalValueVar | SignalValueVar | ||||
| ) | ) | ||||
| @@ -103,9 +104,10 @@ func (v *ValueVar) To(to *Node, slotIdx int) int { | |||||
| return len(v.Toes) - 1 | return len(v.Toes) - 1 | ||||
| } | } | ||||
| func NodeNewOutputValue(node *Node, props any) *ValueVar { | |||||
| func NodeNewOutputValue(node *Node, typ ValueVarType, props any) *ValueVar { | |||||
| val := &ValueVar{ | val := &ValueVar{ | ||||
| ID: node.Graph.genVarID(), | ID: node.Graph.genVarID(), | ||||
| Type: typ, | |||||
| From: EndPoint{Node: node, SlotIndex: len(node.OutputStreams)}, | From: EndPoint{Node: node, SlotIndex: len(node.OutputStreams)}, | ||||
| Props: props, | Props: props, | ||||
| } | } | ||||
| @@ -19,6 +19,7 @@ var opUnion = serder.UseTypeUnionExternallyTagged(types.Ref(types.NewTypeUnion[O | |||||
| type Op interface { | type Op interface { | ||||
| Execute(ctx context.Context, e *Executor) error | Execute(ctx context.Context, e *Executor) error | ||||
| String() string | |||||
| } | } | ||||
| func UseOp[T Op]() { | func UseOp[T Op]() { | ||||
| @@ -2,6 +2,7 @@ package exec | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "strings" | |||||
| "gitlink.org.cn/cloudream/common/pkgs/future" | "gitlink.org.cn/cloudream/common/pkgs/future" | ||||
| "gitlink.org.cn/cloudream/common/utils/lo2" | "gitlink.org.cn/cloudream/common/utils/lo2" | ||||
| @@ -97,6 +98,29 @@ func (b *PlanBuilder) Execute() *Driver { | |||||
| return &exec | return &exec | ||||
| } | } | ||||
| func (b *PlanBuilder) String() string { | |||||
| sb := strings.Builder{} | |||||
| sb.WriteString("Driver:\n") | |||||
| for _, op := range b.DriverPlan.Ops { | |||||
| sb.WriteString(op.String()) | |||||
| sb.WriteRune('\n') | |||||
| } | |||||
| sb.WriteRune('\n') | |||||
| for _, w := range b.WorkerPlans { | |||||
| sb.WriteString("Worker(") | |||||
| sb.WriteString(w.Worker.String()) | |||||
| sb.WriteString("):\n") | |||||
| for _, op := range w.Ops { | |||||
| sb.WriteString(op.String()) | |||||
| sb.WriteRune('\n') | |||||
| } | |||||
| sb.WriteRune('\n') | |||||
| } | |||||
| return sb.String() | |||||
| } | |||||
| type WorkerPlanBuilder struct { | type WorkerPlanBuilder struct { | ||||
| Worker WorkerInfo | Worker WorkerInfo | ||||
| Ops []Op | Ops []Op | ||||
| @@ -1,10 +1,11 @@ | |||||
| package plan | package plan | ||||
| import ( | import ( | ||||
| "fmt" | |||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/dag" | "gitlink.org.cn/cloudream/common/pkgs/ioswitch/dag" | ||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | ||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/plan/ops" | "gitlink.org.cn/cloudream/common/pkgs/ioswitch/plan/ops" | ||||
| "gitlink.org.cn/cloudream/storage/common/pkgs/ioswitch" | |||||
| ) | ) | ||||
| func Generate(graph *dag.Graph, planBld *exec.PlanBuilder) error { | func Generate(graph *dag.Graph, planBld *exec.PlanBuilder) error { | ||||
| @@ -15,6 +16,19 @@ func Generate(graph *dag.Graph, planBld *exec.PlanBuilder) error { | |||||
| // 生成Send指令 | // 生成Send指令 | ||||
| func generateSend(graph *dag.Graph) { | func generateSend(graph *dag.Graph) { | ||||
| graph.Walk(func(node *dag.Node) bool { | graph.Walk(func(node *dag.Node) bool { | ||||
| switch node.Type.(type) { | |||||
| case *ops.SendStreamType: | |||||
| return true | |||||
| case *ops.SendVarType: | |||||
| return true | |||||
| case *ops.GetStreamType: | |||||
| return true | |||||
| case *ops.GetVaType: | |||||
| return true | |||||
| case *ops.HoldUntilType: | |||||
| return true | |||||
| } | |||||
| for _, out := range node.OutputStreams { | for _, out := range node.OutputStreams { | ||||
| to := out.Toes[0] | to := out.Toes[0] | ||||
| if to.Node.Env.Equals(node.Env) { | if to.Node.Env.Equals(node.Env) { | ||||
| @@ -24,30 +38,34 @@ func generateSend(graph *dag.Graph) { | |||||
| switch to.Node.Env.Type { | switch to.Node.Env.Type { | ||||
| case dag.EnvDriver: | case dag.EnvDriver: | ||||
| // // 如果是要送到Driver,则只能由Driver主动去拉取 | // // 如果是要送到Driver,则只能由Driver主动去拉取 | ||||
| getNode := graph.NewNode(&ops.GetStreamType{}, ioswitch.NodeProps{}) | |||||
| getNode, getType := dag.NewNode(graph, &ops.GetStreamType{ | |||||
| FromWorker: node.Env.Worker, | |||||
| }, nil) | |||||
| getNode.Env.ToEnvDriver() | getNode.Env.ToEnvDriver() | ||||
| // // 同时需要对此变量生成HoldUntil指令,避免Plan结束时Get指令还未到达 | // // 同时需要对此变量生成HoldUntil指令,避免Plan结束时Get指令还未到达 | ||||
| holdNode := graph.NewNode(&ops.HoldUntilType{}, ioswitch.NodeProps{}) | |||||
| holdNode, holdType := dag.NewNode(graph, &ops.HoldUntilType{}, nil) | |||||
| holdNode.Env = node.Env | holdNode.Env = node.Env | ||||
| // 将Get指令的信号送到Hold指令 | // 将Get指令的信号送到Hold指令 | ||||
| getNode.OutputValues[0].To(holdNode, 0) | |||||
| // 将Get指令的输出送到目的地 | |||||
| getNode.OutputStreams[0].To(to.Node, to.SlotIndex) | |||||
| holdType.Signal(holdNode, getType.SignalVar(getNode)) | |||||
| out.Toes = nil | out.Toes = nil | ||||
| // 将源节点的输出送到Hold指令 | |||||
| out.To(holdNode, 0) | |||||
| // 将Hold指令的输出送到Get指令 | |||||
| holdNode.OutputStreams[0].To(getNode, 0) | |||||
| // 将源节点的输出送到Hold指令,将Hold指令的输出送到Get指令 | |||||
| getType.Get(getNode, holdType.HoldStream(holdNode, out)). | |||||
| // 将Get指令的输出送到目的地 | |||||
| To(to.Node, to.SlotIndex) | |||||
| case dag.EnvWorker: | case dag.EnvWorker: | ||||
| // 如果是要送到Agent,则可以直接发送 | // 如果是要送到Agent,则可以直接发送 | ||||
| n := graph.NewNode(&ops.SendStreamType{}, ioswitch.NodeProps{}) | |||||
| n, t := dag.NewNode(graph, &ops.SendStreamType{ | |||||
| ToWorker: to.Node.Env.Worker, | |||||
| }, nil) | |||||
| n.Env = node.Env | n.Env = node.Env | ||||
| n.OutputStreams[0].To(to.Node, to.SlotIndex) | |||||
| out.Toes = nil | out.Toes = nil | ||||
| out.To(n, 0) | |||||
| t.Send(n, out).To(to.Node, to.SlotIndex) | |||||
| } | } | ||||
| } | } | ||||
| @@ -60,30 +78,34 @@ func generateSend(graph *dag.Graph) { | |||||
| switch to.Node.Env.Type { | switch to.Node.Env.Type { | ||||
| case dag.EnvDriver: | case dag.EnvDriver: | ||||
| // // 如果是要送到Driver,则只能由Driver主动去拉取 | // // 如果是要送到Driver,则只能由Driver主动去拉取 | ||||
| getNode := graph.NewNode(&ops.GetVaType{}, ioswitch.NodeProps{}) | |||||
| getNode, getType := dag.NewNode(graph, &ops.GetVaType{ | |||||
| FromWorker: node.Env.Worker, | |||||
| }, nil) | |||||
| getNode.Env.ToEnvDriver() | getNode.Env.ToEnvDriver() | ||||
| // // 同时需要对此变量生成HoldUntil指令,避免Plan结束时Get指令还未到达 | // // 同时需要对此变量生成HoldUntil指令,避免Plan结束时Get指令还未到达 | ||||
| holdNode := graph.NewNode(&ops.HoldUntilType{}, ioswitch.NodeProps{}) | |||||
| holdNode, holdType := dag.NewNode(graph, &ops.HoldUntilType{}, nil) | |||||
| holdNode.Env = node.Env | holdNode.Env = node.Env | ||||
| // 将Get指令的信号送到Hold指令 | // 将Get指令的信号送到Hold指令 | ||||
| getNode.OutputValues[0].To(holdNode, 0) | |||||
| // 将Get指令的输出送到目的地 | |||||
| getNode.OutputValues[1].To(to.Node, to.SlotIndex) | |||||
| holdType.Signal(holdNode, getType.SignalVar(getNode)) | |||||
| out.Toes = nil | out.Toes = nil | ||||
| // 将源节点的输出送到Hold指令 | |||||
| out.To(holdNode, 0) | |||||
| // 将Hold指令的输出送到Get指令 | |||||
| holdNode.OutputValues[0].To(getNode, 0) | |||||
| // 将源节点的输出送到Hold指令,将Hold指令的输出送到Get指令 | |||||
| getType.Get(getNode, holdType.HoldVar(holdNode, out)). | |||||
| // 将Get指令的输出送到目的地 | |||||
| To(to.Node, to.SlotIndex) | |||||
| case dag.EnvWorker: | case dag.EnvWorker: | ||||
| // 如果是要送到Agent,则可以直接发送 | // 如果是要送到Agent,则可以直接发送 | ||||
| n := graph.NewNode(&ops.SendVarType{}, ioswitch.NodeProps{}) | |||||
| n, t := dag.NewNode(graph, &ops.SendVarType{ | |||||
| ToWorker: to.Node.Env.Worker, | |||||
| }, nil) | |||||
| n.Env = node.Env | n.Env = node.Env | ||||
| n.OutputValues[0].To(to.Node, to.SlotIndex) | |||||
| out.Toes = nil | out.Toes = nil | ||||
| out.To(n, 0) | |||||
| t.Send(n, out).To(to.Node, to.SlotIndex) | |||||
| } | } | ||||
| } | } | ||||
| @@ -121,6 +143,9 @@ func buildPlan(graph *dag.Graph, blder *exec.PlanBuilder) error { | |||||
| out.Var = blder.NewStringVar() | out.Var = blder.NewStringVar() | ||||
| case dag.SignalValueVar: | case dag.SignalValueVar: | ||||
| out.Var = blder.NewSignalVar() | out.Var = blder.NewSignalVar() | ||||
| default: | |||||
| retErr = fmt.Errorf("unsupported value var type: %v", out.Type) | |||||
| return false | |||||
| } | } | ||||
| } | } | ||||
| @@ -134,6 +159,9 @@ func buildPlan(graph *dag.Graph, blder *exec.PlanBuilder) error { | |||||
| in.Var = blder.NewStringVar() | in.Var = blder.NewStringVar() | ||||
| case dag.SignalValueVar: | case dag.SignalValueVar: | ||||
| in.Var = blder.NewSignalVar() | in.Var = blder.NewSignalVar() | ||||
| default: | |||||
| retErr = fmt.Errorf("unsupported value var type: %v", in.Type) | |||||
| return false | |||||
| } | } | ||||
| } | } | ||||
| @@ -143,6 +171,11 @@ func buildPlan(graph *dag.Graph, blder *exec.PlanBuilder) error { | |||||
| return false | return false | ||||
| } | } | ||||
| // TODO 当前ToDriver,FromDriver不会生成Op,所以这里需要判断一下 | |||||
| if op == nil { | |||||
| return true | |||||
| } | |||||
| switch node.Env.Type { | switch node.Env.Type { | ||||
| case dag.EnvDriver: | case dag.EnvDriver: | ||||
| blder.AtDriver().AddOp(op) | blder.AtDriver().AddOp(op) | ||||
| @@ -5,6 +5,7 @@ import ( | |||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/dag" | "gitlink.org.cn/cloudream/common/pkgs/ioswitch/dag" | ||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | ||||
| "gitlink.org.cn/cloudream/storage/common/pkgs/ioswitch2" | |||||
| ) | ) | ||||
| type FromDriverType struct { | type FromDriverType struct { | ||||
| @@ -12,7 +13,7 @@ type FromDriverType struct { | |||||
| } | } | ||||
| func (t *FromDriverType) InitNode(node *dag.Node) { | func (t *FromDriverType) InitNode(node *dag.Node) { | ||||
| dag.NodeNewOutputStream(node, nil) | |||||
| dag.NodeNewOutputStream(node, &ioswitch2.VarProps{}) | |||||
| } | } | ||||
| func (t *FromDriverType) GenerateOp(op *dag.Node) (exec.Op, error) { | func (t *FromDriverType) GenerateOp(op *dag.Node) (exec.Op, error) { | ||||
| @@ -22,6 +22,7 @@ func (o *DropStream) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| if err != nil { | if err != nil { | ||||
| return err | return err | ||||
| } | } | ||||
| defer o.Input.Stream.Close() | |||||
| for { | for { | ||||
| buf := make([]byte, 1024*8) | buf := make([]byte, 1024*8) | ||||
| @@ -35,6 +36,10 @@ func (o *DropStream) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| } | } | ||||
| } | } | ||||
| func (o *DropStream) String() string { | |||||
| return fmt.Sprintf("DropStream %v", o.Input.ID) | |||||
| } | |||||
| type DropType struct{} | type DropType struct{} | ||||
| func (t *DropType) InitNode(node *dag.Node) { | func (t *DropType) InitNode(node *dag.Node) { | ||||
| @@ -49,6 +49,10 @@ func (o *SendStream) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (o *SendStream) String() string { | |||||
| return fmt.Sprintf("SendStream %v->%v@%v", o.Input.ID, o.Send.ID, o.Worker) | |||||
| } | |||||
| type GetStream struct { | type GetStream struct { | ||||
| Signal *exec.SignalVar `json:"signal"` | Signal *exec.SignalVar `json:"signal"` | ||||
| Target *exec.StreamVar `json:"target"` | Target *exec.StreamVar `json:"target"` | ||||
| @@ -80,6 +84,10 @@ func (o *GetStream) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return fut.Wait(ctx) | return fut.Wait(ctx) | ||||
| } | } | ||||
| func (o *GetStream) String() string { | |||||
| return fmt.Sprintf("GetStream %v(S:%v)<-%v@%v", o.Output.ID, o.Signal.ID, o.Target.ID, o.Worker) | |||||
| } | |||||
| type SendVar struct { | type SendVar struct { | ||||
| Input exec.Var `json:"input"` | Input exec.Var `json:"input"` | ||||
| Send exec.Var `json:"send"` | Send exec.Var `json:"send"` | ||||
| @@ -109,6 +117,10 @@ func (o *SendVar) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (o *SendVar) String() string { | |||||
| return fmt.Sprintf("SendVar %v->%v@%v", o.Input.GetID(), o.Send.GetID(), o.Worker) | |||||
| } | |||||
| type GetVar struct { | type GetVar struct { | ||||
| Signal *exec.SignalVar `json:"signal"` | Signal *exec.SignalVar `json:"signal"` | ||||
| Target exec.Var `json:"target"` | Target exec.Var `json:"target"` | ||||
| @@ -135,10 +147,19 @@ func (o *GetVar) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (o *GetVar) String() string { | |||||
| return fmt.Sprintf("GetVar %v(S:%v)<-%v@%v", o.Output.GetID(), o.Signal.ID, o.Target.GetID(), o.Worker) | |||||
| } | |||||
| type SendStreamType struct { | type SendStreamType struct { | ||||
| ToWorker exec.WorkerInfo | ToWorker exec.WorkerInfo | ||||
| } | } | ||||
| func (t *SendStreamType) Send(n *dag.Node, v *dag.StreamVar) *dag.StreamVar { | |||||
| v.To(n, 0) | |||||
| return n.OutputStreams[0] | |||||
| } | |||||
| func (t *SendStreamType) InitNode(node *dag.Node) { | func (t *SendStreamType) InitNode(node *dag.Node) { | ||||
| dag.NodeDeclareInputStream(node, 1) | dag.NodeDeclareInputStream(node, 1) | ||||
| dag.NodeNewOutputStream(node, nil) | dag.NodeNewOutputStream(node, nil) | ||||
| @@ -160,9 +181,15 @@ type SendVarType struct { | |||||
| ToWorker exec.WorkerInfo | ToWorker exec.WorkerInfo | ||||
| } | } | ||||
| func (t *SendVarType) Send(n *dag.Node, v *dag.ValueVar) *dag.ValueVar { | |||||
| v.To(n, 0) | |||||
| n.OutputValues[0].Type = v.Type | |||||
| return n.OutputValues[0] | |||||
| } | |||||
| func (t *SendVarType) InitNode(node *dag.Node) { | func (t *SendVarType) InitNode(node *dag.Node) { | ||||
| dag.NodeDeclareInputValue(node, 1) | dag.NodeDeclareInputValue(node, 1) | ||||
| dag.NodeNewOutputValue(node, nil) | |||||
| dag.NodeNewOutputValue(node, 0, nil) | |||||
| } | } | ||||
| func (t *SendVarType) GenerateOp(op *dag.Node) (exec.Op, error) { | func (t *SendVarType) GenerateOp(op *dag.Node) (exec.Op, error) { | ||||
| @@ -181,9 +208,18 @@ type GetStreamType struct { | |||||
| FromWorker exec.WorkerInfo | FromWorker exec.WorkerInfo | ||||
| } | } | ||||
| func (t *GetStreamType) Get(n *dag.Node, v *dag.StreamVar) *dag.StreamVar { | |||||
| v.To(n, 0) | |||||
| return n.OutputStreams[0] | |||||
| } | |||||
| func (t *GetStreamType) SignalVar(n *dag.Node) *dag.ValueVar { | |||||
| return n.OutputValues[0] | |||||
| } | |||||
| func (t *GetStreamType) InitNode(node *dag.Node) { | func (t *GetStreamType) InitNode(node *dag.Node) { | ||||
| dag.NodeDeclareInputStream(node, 1) | dag.NodeDeclareInputStream(node, 1) | ||||
| dag.NodeNewOutputValue(node, nil) | |||||
| dag.NodeNewOutputValue(node, dag.SignalValueVar, nil) | |||||
| dag.NodeNewOutputStream(node, nil) | dag.NodeNewOutputStream(node, nil) | ||||
| } | } | ||||
| @@ -204,10 +240,20 @@ type GetVaType struct { | |||||
| FromWorker exec.WorkerInfo | FromWorker exec.WorkerInfo | ||||
| } | } | ||||
| func (t *GetVaType) Get(n *dag.Node, v *dag.ValueVar) *dag.ValueVar { | |||||
| v.To(n, 0) | |||||
| n.OutputValues[1].Type = v.Type | |||||
| return n.OutputValues[1] | |||||
| } | |||||
| func (t *GetVaType) SignalVar(n *dag.Node) *dag.ValueVar { | |||||
| return n.OutputValues[0] | |||||
| } | |||||
| func (t *GetVaType) InitNode(node *dag.Node) { | func (t *GetVaType) InitNode(node *dag.Node) { | ||||
| dag.NodeDeclareInputValue(node, 1) | dag.NodeDeclareInputValue(node, 1) | ||||
| dag.NodeNewOutputValue(node, nil) | |||||
| dag.NodeNewOutputValue(node, nil) | |||||
| dag.NodeNewOutputValue(node, dag.SignalValueVar, nil) | |||||
| dag.NodeNewOutputValue(node, 0, nil) | |||||
| } | } | ||||
| func (t *GetVaType) GenerateOp(op *dag.Node) (exec.Op, error) { | func (t *GetVaType) GenerateOp(op *dag.Node) (exec.Op, error) { | ||||
| @@ -29,10 +29,18 @@ func (o *Store) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (o *Store) String() string { | |||||
| return fmt.Sprintf("Store %v: %v", o.Key, o.Var.GetID()) | |||||
| } | |||||
| type StoreType struct { | type StoreType struct { | ||||
| StoreKey string | StoreKey string | ||||
| } | } | ||||
| func (t *StoreType) Store(node *dag.Node, v *dag.ValueVar) { | |||||
| v.To(node, 0) | |||||
| } | |||||
| func (t *StoreType) InitNode(node *dag.Node) { | func (t *StoreType) InitNode(node *dag.Node) { | ||||
| dag.NodeDeclareInputValue(node, 1) | dag.NodeDeclareInputValue(node, 1) | ||||
| } | } | ||||
| @@ -8,6 +8,7 @@ import ( | |||||
| "gitlink.org.cn/cloudream/common/pkgs/future" | "gitlink.org.cn/cloudream/common/pkgs/future" | ||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/dag" | "gitlink.org.cn/cloudream/common/pkgs/ioswitch/dag" | ||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | ||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/utils" | |||||
| ) | ) | ||||
| func init() { | func init() { | ||||
| @@ -36,6 +37,10 @@ func (o *OnStreamBegin) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (o *OnStreamBegin) String() string { | |||||
| return fmt.Sprintf("OnStreamBegin %v->%v S:%v", o.Raw.ID, o.New.ID, o.Signal.ID) | |||||
| } | |||||
| type OnStreamEnd struct { | type OnStreamEnd struct { | ||||
| Raw *exec.StreamVar `json:"raw"` | Raw *exec.StreamVar `json:"raw"` | ||||
| New *exec.StreamVar `json:"new"` | New *exec.StreamVar `json:"new"` | ||||
| @@ -85,6 +90,10 @@ func (o *OnStreamEnd) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (o *OnStreamEnd) String() string { | |||||
| return fmt.Sprintf("OnStreamEnd %v->%v S:%v", o.Raw.ID, o.New.ID, o.Signal.ID) | |||||
| } | |||||
| type HoldUntil struct { | type HoldUntil struct { | ||||
| Waits []*exec.SignalVar `json:"waits"` | Waits []*exec.SignalVar `json:"waits"` | ||||
| Holds []exec.Var `json:"holds"` | Holds []exec.Var `json:"holds"` | ||||
| @@ -113,6 +122,10 @@ func (w *HoldUntil) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (w *HoldUntil) String() string { | |||||
| return fmt.Sprintf("HoldUntil Waits: %v, (%v) -> (%v)", utils.FormatVarIDs(w.Waits), utils.FormatVarIDs(w.Holds), utils.FormatVarIDs(w.Emits)) | |||||
| } | |||||
| type HangUntil struct { | type HangUntil struct { | ||||
| Waits []*exec.SignalVar `json:"waits"` | Waits []*exec.SignalVar `json:"waits"` | ||||
| Op exec.Op `json:"op"` | Op exec.Op `json:"op"` | ||||
| @@ -127,6 +140,10 @@ func (h *HangUntil) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return h.Op.Execute(ctx, e) | return h.Op.Execute(ctx, e) | ||||
| } | } | ||||
| func (h *HangUntil) String() string { | |||||
| return "HangUntil" | |||||
| } | |||||
| type Broadcast struct { | type Broadcast struct { | ||||
| Source *exec.SignalVar `json:"source"` | Source *exec.SignalVar `json:"source"` | ||||
| Targets []*exec.SignalVar `json:"targets"` | Targets []*exec.SignalVar `json:"targets"` | ||||
| @@ -142,6 +159,10 @@ func (b *Broadcast) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| return nil | return nil | ||||
| } | } | ||||
| func (b *Broadcast) String() string { | |||||
| return "Broadcast" | |||||
| } | |||||
| type HoldUntilType struct { | type HoldUntilType struct { | ||||
| } | } | ||||
| @@ -167,6 +188,24 @@ func (t *HoldUntilType) GenerateOp(op *dag.Node) (exec.Op, error) { | |||||
| return o, nil | return o, nil | ||||
| } | } | ||||
| func (t *HoldUntilType) Signal(n *dag.Node, s *dag.ValueVar) { | |||||
| s.To(n, 0) | |||||
| } | |||||
| func (t *HoldUntilType) HoldStream(n *dag.Node, str *dag.StreamVar) *dag.StreamVar { | |||||
| n.InputStreams = append(n.InputStreams, nil) | |||||
| str.To(n, len(n.InputStreams)-1) | |||||
| return dag.NodeNewOutputStream(n, nil) | |||||
| } | |||||
| func (t *HoldUntilType) HoldVar(n *dag.Node, v *dag.ValueVar) *dag.ValueVar { | |||||
| n.InputValues = append(n.InputValues, nil) | |||||
| v.To(n, len(n.InputValues)-1) | |||||
| return dag.NodeNewOutputValue(n, v.Type, nil) | |||||
| } | |||||
| func (t *HoldUntilType) String(node *dag.Node) string { | func (t *HoldUntilType) String(node *dag.Node) string { | ||||
| return fmt.Sprintf("HoldUntil[]%v%v", formatStreamIO(node), formatValueIO(node)) | return fmt.Sprintf("HoldUntil[]%v%v", formatStreamIO(node), formatValueIO(node)) | ||||
| } | } | ||||
| @@ -18,3 +18,7 @@ func (o *ConstVar) Execute(ctx context.Context, e *exec.Executor) error { | |||||
| e.PutVars(o.Var) | e.PutVars(o.Var) | ||||
| return nil | return nil | ||||
| } | } | ||||
| func (o *ConstVar) String() string { | |||||
| return "ConstVar" | |||||
| } | |||||
| @@ -0,0 +1,19 @@ | |||||
| package utils | |||||
| import ( | |||||
| "fmt" | |||||
| "strings" | |||||
| "gitlink.org.cn/cloudream/common/pkgs/ioswitch/exec" | |||||
| ) | |||||
| func FormatVarIDs[T exec.Var](arr []T) string { | |||||
| sb := strings.Builder{} | |||||
| for i, v := range arr { | |||||
| sb.WriteString(fmt.Sprintf("%v", v.GetID())) | |||||
| if i < len(arr)-1 { | |||||
| sb.WriteString(",") | |||||
| } | |||||
| } | |||||
| return sb.String() | |||||
| } | |||||
| @@ -37,6 +37,7 @@ var RedundancyUnion = serder.UseTypeUnionInternallyTagged(types.Ref(types.NewTyp | |||||
| (*NoneRedundancy)(nil), | (*NoneRedundancy)(nil), | ||||
| (*RepRedundancy)(nil), | (*RepRedundancy)(nil), | ||||
| (*ECRedundancy)(nil), | (*ECRedundancy)(nil), | ||||
| (*LRCRedundancy)(nil), | |||||
| )), "type") | )), "type") | ||||
| type NoneRedundancy struct { | type NoneRedundancy struct { | ||||
| @@ -93,6 +94,67 @@ func (b *ECRedundancy) Value() (driver.Value, error) { | |||||
| return serder.ObjectToJSONEx[Redundancy](b) | return serder.ObjectToJSONEx[Redundancy](b) | ||||
| } | } | ||||
| var DefaultLRCRedundancy = *NewLRCRedundancy(2, 4, []int{2}, 1024*1024*5) | |||||
| type LRCRedundancy struct { | |||||
| serder.Metadata `union:"lrc"` | |||||
| Type string `json:"type"` | |||||
| K int `json:"k"` | |||||
| N int `json:"n"` | |||||
| Groups []int `json:"groups"` | |||||
| ChunkSize int `json:"chunkSize"` | |||||
| } | |||||
| func NewLRCRedundancy(k int, n int, groups []int, chunkSize int) *LRCRedundancy { | |||||
| return &LRCRedundancy{ | |||||
| Type: "lrc", | |||||
| K: k, | |||||
| N: n, | |||||
| Groups: groups, | |||||
| ChunkSize: chunkSize, | |||||
| } | |||||
| } | |||||
| func (b *LRCRedundancy) Value() (driver.Value, error) { | |||||
| return serder.ObjectToJSONEx[Redundancy](b) | |||||
| } | |||||
| // 判断指定块属于哪个组。如果都不属于,则返回-1。 | |||||
| func (b *LRCRedundancy) FindGroup(idx int) int { | |||||
| if idx >= b.N-len(b.Groups) { | |||||
| return idx - (b.N - len(b.Groups)) | |||||
| } | |||||
| for i, group := range b.Groups { | |||||
| if idx < group { | |||||
| return i | |||||
| } | |||||
| idx -= group | |||||
| } | |||||
| return -1 | |||||
| } | |||||
| // M = N - len(Groups),即数据块+校验块的总数,不包括组校验块。 | |||||
| func (b *LRCRedundancy) M() int { | |||||
| return b.N - len(b.Groups) | |||||
| } | |||||
| func (b *LRCRedundancy) GetGroupElements(grp int) []int { | |||||
| var idxes []int | |||||
| grpStart := 0 | |||||
| for i := 0; i < grp; i++ { | |||||
| grpStart += b.Groups[i] | |||||
| } | |||||
| for i := 0; i < b.Groups[grp]; i++ { | |||||
| idxes = append(idxes, grpStart+i) | |||||
| } | |||||
| idxes = append(idxes, b.N-len(b.Groups)+grp) | |||||
| return idxes | |||||
| } | |||||
| const ( | const ( | ||||
| PackageStateNormal = "Normal" | PackageStateNormal = "Normal" | ||||
| PackageStateDeleted = "Deleted" | PackageStateDeleted = "Deleted" | ||||
| @@ -11,6 +11,12 @@ func Remove[T comparable](arr []T, item T) []T { | |||||
| return RemoveAt(arr, index) | return RemoveAt(arr, index) | ||||
| } | } | ||||
| func RemoveAll[T comparable](arr []T, item T) []T { | |||||
| return lo.Filter(arr, func(i T, idx int) bool { | |||||
| return i != item | |||||
| }) | |||||
| } | |||||
| func RemoveAt[T any](arr []T, index int) []T { | func RemoveAt[T any](arr []T, index int) []T { | ||||
| if index >= len(arr) { | if index >= len(arr) { | ||||
| return arr | return arr | ||||
| @@ -0,0 +1,72 @@ | |||||
| package json | |||||
| import ( | |||||
| "reflect" | |||||
| jsoniter "github.com/json-iterator/go" | |||||
| "gitlink.org.cn/cloudream/common/pkgs/types" | |||||
| ) | |||||
| type Config struct { | |||||
| unionHandler *UnionHandler | |||||
| exts []jsoniter.Extension | |||||
| } | |||||
| func New() *Config { | |||||
| return &Config{ | |||||
| unionHandler: &UnionHandler{ | |||||
| internallyTagged: make(map[reflect.Type]*anyTypeUnionInternallyTagged), | |||||
| externallyTagged: make(map[reflect.Type]*anyTypeUnionExternallyTagged), | |||||
| }, | |||||
| } | |||||
| } | |||||
| func (c *Config) UseUnionInternallyTagged(u *types.AnyTypeUnion, tagField string) *Config { | |||||
| iu := &anyTypeUnionInternallyTagged{ | |||||
| Union: u, | |||||
| TagField: tagField, | |||||
| TagToType: make(map[string]reflect.Type), | |||||
| } | |||||
| for _, eleType := range u.ElementTypes { | |||||
| iu.Add(eleType) | |||||
| } | |||||
| c.unionHandler.internallyTagged[u.UnionType] = iu | |||||
| return c | |||||
| } | |||||
| func (c *Config) UseUnionExternallyTagged(u *types.AnyTypeUnion) *Config { | |||||
| eu := &anyTypeUnionExternallyTagged{ | |||||
| Union: u, | |||||
| TypeNameToType: make(map[string]reflect.Type), | |||||
| } | |||||
| for _, eleType := range u.ElementTypes { | |||||
| eu.Add(eleType) | |||||
| } | |||||
| c.unionHandler.externallyTagged[u.UnionType] = eu | |||||
| return c | |||||
| } | |||||
| func (c *Config) UseExtension(ext jsoniter.Extension) *Config { | |||||
| c.exts = append(c.exts, ext) | |||||
| return c | |||||
| } | |||||
| func (c *Config) Build() Serder { | |||||
| cfg := jsoniter.Config{} | |||||
| api := cfg.Froze() | |||||
| api.RegisterExtension(c.unionHandler) | |||||
| for _, ext := range c.exts { | |||||
| api.RegisterExtension(ext) | |||||
| } | |||||
| return Serder{ | |||||
| cfg: *c, | |||||
| api: api, | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,29 @@ | |||||
| package json | |||||
| import ( | |||||
| "bytes" | |||||
| jsoniter "github.com/json-iterator/go" | |||||
| ) | |||||
| type Serder struct { | |||||
| cfg Config | |||||
| api jsoniter.API | |||||
| } | |||||
| func (s *Serder) Encode(obj any) ([]byte, error) { | |||||
| buf := new(bytes.Buffer) | |||||
| enc := s.api.NewEncoder(buf) | |||||
| err := enc.Encode(obj) | |||||
| if err != nil { | |||||
| return nil, err | |||||
| } | |||||
| return buf.Bytes(), nil | |||||
| } | |||||
| func (s *Serder) Decode(data []byte, obj any) error { | |||||
| dec := s.api.NewDecoder(bytes.NewReader(data)) | |||||
| return dec.Decode(&obj) | |||||
| } | |||||
| @@ -0,0 +1,318 @@ | |||||
| package json | |||||
| import ( | |||||
| "fmt" | |||||
| "reflect" | |||||
| "unsafe" | |||||
| jsoniter "github.com/json-iterator/go" | |||||
| "github.com/modern-go/reflect2" | |||||
| "gitlink.org.cn/cloudream/common/pkgs/types" | |||||
| stypes "gitlink.org.cn/cloudream/common/utils/serder/types" | |||||
| ref2 "gitlink.org.cn/cloudream/common/utils/reflect2" | |||||
| ) | |||||
| type anyTypeUnionExternallyTagged struct { | |||||
| Union *types.AnyTypeUnion | |||||
| TypeNameToType map[string]reflect.Type | |||||
| } | |||||
| func (u *anyTypeUnionExternallyTagged) Add(typ reflect.Type) error { | |||||
| err := u.Union.Add(typ) | |||||
| if err != nil { | |||||
| return nil | |||||
| } | |||||
| u.TypeNameToType[makeDerefFullTypeName(typ)] = typ | |||||
| return nil | |||||
| } | |||||
| type TypeUnionExternallyTagged[T any] struct { | |||||
| anyTypeUnionExternallyTagged | |||||
| TUnion *types.TypeUnion[T] | |||||
| } | |||||
| func (u *TypeUnionExternallyTagged[T]) AddT(nilValue T) error { | |||||
| u.Add(reflect.TypeOf(nilValue)) | |||||
| return nil | |||||
| } | |||||
| type anyTypeUnionInternallyTagged struct { | |||||
| Union *types.AnyTypeUnion | |||||
| TagField string | |||||
| TagToType map[string]reflect.Type | |||||
| } | |||||
| func (u *anyTypeUnionInternallyTagged) Add(typ reflect.Type) error { | |||||
| err := u.Union.Add(typ) | |||||
| if err != nil { | |||||
| return nil | |||||
| } | |||||
| // 解引用直到得到结构体类型 | |||||
| structType := typ | |||||
| for structType.Kind() == reflect.Pointer { | |||||
| structType = structType.Elem() | |||||
| } | |||||
| // 要求内嵌Metadata结构体,那么结构体中的字段名就会是Metadata, | |||||
| field, ok := structType.FieldByName(ref2.TypeNameOf[stypes.Metadata]()) | |||||
| if !ok { | |||||
| u.TagToType[makeDerefFullTypeName(structType)] = typ | |||||
| return nil | |||||
| } | |||||
| // 为防同名,检查类型是不是也是Metadata | |||||
| if field.Type != ref2.TypeOf[stypes.Metadata]() { | |||||
| u.TagToType[makeDerefFullTypeName(structType)] = typ | |||||
| return nil | |||||
| } | |||||
| tag := field.Tag.Get("union") | |||||
| if tag == "" { | |||||
| u.TagToType[makeDerefFullTypeName(structType)] = typ | |||||
| return nil | |||||
| } | |||||
| u.TagToType[tag] = typ | |||||
| return nil | |||||
| } | |||||
| type TypeUnionInternallyTagged[T any] struct { | |||||
| anyTypeUnionInternallyTagged | |||||
| TUnion *types.TypeUnion[T] | |||||
| } | |||||
| func (u *TypeUnionInternallyTagged[T]) AddT(nilValue T) error { | |||||
| u.Add(reflect.TypeOf(nilValue)) | |||||
| return nil | |||||
| } | |||||
| type UnionHandler struct { | |||||
| internallyTagged map[reflect.Type]*anyTypeUnionInternallyTagged | |||||
| externallyTagged map[reflect.Type]*anyTypeUnionExternallyTagged | |||||
| } | |||||
| func (h *UnionHandler) UpdateStructDescriptor(structDescriptor *jsoniter.StructDescriptor) { | |||||
| } | |||||
| func (h *UnionHandler) CreateMapKeyDecoder(typ reflect2.Type) jsoniter.ValDecoder { | |||||
| return nil | |||||
| } | |||||
| func (h *UnionHandler) CreateMapKeyEncoder(typ reflect2.Type) jsoniter.ValEncoder { | |||||
| return nil | |||||
| } | |||||
| func (h *UnionHandler) CreateDecoder(typ reflect2.Type) jsoniter.ValDecoder { | |||||
| typ1 := typ.Type1() | |||||
| if it, ok := h.internallyTagged[typ1]; ok { | |||||
| return &InternallyTaggedDecoder{ | |||||
| union: it, | |||||
| } | |||||
| } | |||||
| if et, ok := h.externallyTagged[typ1]; ok { | |||||
| return &ExternallyTaggedDecoder{ | |||||
| union: et, | |||||
| } | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (h *UnionHandler) CreateEncoder(typ reflect2.Type) jsoniter.ValEncoder { | |||||
| typ1 := typ.Type1() | |||||
| if it, ok := h.internallyTagged[typ1]; ok { | |||||
| return &InternallyTaggedEncoder{ | |||||
| union: it, | |||||
| } | |||||
| } | |||||
| if et, ok := h.externallyTagged[typ1]; ok { | |||||
| return &ExternallyTaggedEncoder{ | |||||
| union: et, | |||||
| } | |||||
| } | |||||
| return nil | |||||
| } | |||||
| func (h *UnionHandler) DecorateDecoder(typ reflect2.Type, decoder jsoniter.ValDecoder) jsoniter.ValDecoder { | |||||
| return decoder | |||||
| } | |||||
| func (h *UnionHandler) DecorateEncoder(typ reflect2.Type, encoder jsoniter.ValEncoder) jsoniter.ValEncoder { | |||||
| return encoder | |||||
| } | |||||
| // 以下Encoder/Decoder都是在传入类型/目标类型是TypeUnion的基类(UnionType)时使用 | |||||
| type InternallyTaggedEncoder struct { | |||||
| union *anyTypeUnionInternallyTagged | |||||
| } | |||||
| func (e *InternallyTaggedEncoder) IsEmpty(ptr unsafe.Pointer) bool { | |||||
| return false | |||||
| } | |||||
| func (e *InternallyTaggedEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { | |||||
| var val any | |||||
| if e.union.Union.UnionType.NumMethod() == 0 { | |||||
| // 无方法的interface底层都是eface结构体,所以可以直接转*any | |||||
| val = *(*any)(ptr) | |||||
| } else { | |||||
| // 有方法的interface底层都是iface结构体,可以将其转成eface,转换后不损失类型信息 | |||||
| val = reflect2.IFaceToEFace(ptr) | |||||
| } | |||||
| // 可以考虑检查一下Type字段有没有赋值,没有赋值则将其赋值为union Tag指定的值 | |||||
| stream.WriteVal(val) | |||||
| } | |||||
| type InternallyTaggedDecoder struct { | |||||
| union *anyTypeUnionInternallyTagged | |||||
| } | |||||
| func (e *InternallyTaggedDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { | |||||
| nextTokenKind := iter.WhatIsNext() | |||||
| if nextTokenKind == jsoniter.NilValue { | |||||
| iter.Skip() | |||||
| return | |||||
| } | |||||
| raw := iter.ReadAny() | |||||
| if raw.LastError() != nil { | |||||
| iter.ReportError("decode TaggedUnionType", "getting object raw:"+raw.LastError().Error()) | |||||
| return | |||||
| } | |||||
| tagField := raw.Get(e.union.TagField) | |||||
| if tagField.LastError() != nil { | |||||
| iter.ReportError("decode TaggedUnionType", "getting type tag field:"+tagField.LastError().Error()) | |||||
| return | |||||
| } | |||||
| typeTag := tagField.ToString() | |||||
| if typeTag == "" { | |||||
| iter.ReportError("decode TaggedUnionType", "type tag is empty") | |||||
| return | |||||
| } | |||||
| typ, ok := e.union.TagToType[typeTag] | |||||
| if !ok { | |||||
| iter.ReportError("decode TaggedUnionType", fmt.Sprintf("unknow type tag %s in union %s", typeTag, e.union.Union.UnionType.Name())) | |||||
| return | |||||
| } | |||||
| // 如果目标类型已经是个指针类型*T,那么在New的时候就需要使用T, | |||||
| // 否则New出来的是会是**T,这将导致后续的反序列化出问题 | |||||
| if typ.Kind() == reflect.Pointer { | |||||
| val := reflect.New(typ.Elem()) | |||||
| raw.ToVal(val.Interface()) | |||||
| retVal := reflect.NewAt(e.union.Union.UnionType, ptr) | |||||
| retVal.Elem().Set(val) | |||||
| } else { | |||||
| val := reflect.New(typ) | |||||
| raw.ToVal(val.Interface()) | |||||
| retVal := reflect.NewAt(e.union.Union.UnionType, ptr) | |||||
| retVal.Elem().Set(val.Elem()) | |||||
| } | |||||
| } | |||||
| type ExternallyTaggedEncoder struct { | |||||
| union *anyTypeUnionExternallyTagged | |||||
| } | |||||
| func (e *ExternallyTaggedEncoder) IsEmpty(ptr unsafe.Pointer) bool { | |||||
| return false | |||||
| } | |||||
| func (e *ExternallyTaggedEncoder) Encode(ptr unsafe.Pointer, stream *jsoniter.Stream) { | |||||
| var val any | |||||
| if e.union.Union.UnionType.NumMethod() == 0 { | |||||
| // 无方法的interface底层都是eface结构体,所以可以直接转*any | |||||
| val = *(*any)(ptr) | |||||
| } else { | |||||
| // 有方法的interface底层都是iface结构体,可以将其转成eface,转换后不损失类型信息 | |||||
| val = reflect2.IFaceToEFace(ptr) | |||||
| } | |||||
| if val == nil { | |||||
| stream.WriteNil() | |||||
| return | |||||
| } | |||||
| stream.WriteObjectStart() | |||||
| valType := ref2.TypeOfValue(val) | |||||
| if !e.union.Union.Include(valType) { | |||||
| stream.Error = fmt.Errorf("type %v is not in union %v", valType, e.union.Union.UnionType) | |||||
| return | |||||
| } | |||||
| stream.WriteObjectField(makeDerefFullTypeName(valType)) | |||||
| stream.WriteVal(val) | |||||
| stream.WriteObjectEnd() | |||||
| } | |||||
| type ExternallyTaggedDecoder struct { | |||||
| union *anyTypeUnionExternallyTagged | |||||
| } | |||||
| func (e *ExternallyTaggedDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) { | |||||
| nextTkType := iter.WhatIsNext() | |||||
| if nextTkType == jsoniter.NilValue { | |||||
| iter.Skip() | |||||
| return | |||||
| } | |||||
| if nextTkType != jsoniter.ObjectValue { | |||||
| iter.ReportError("decode UnionType", fmt.Sprintf("unknow next token type %v", nextTkType)) | |||||
| return | |||||
| } | |||||
| typeStr := iter.ReadObject() | |||||
| if typeStr == "" { | |||||
| iter.ReportError("decode UnionType", "type string is empty") | |||||
| } | |||||
| typ, ok := e.union.TypeNameToType[typeStr] | |||||
| if !ok { | |||||
| iter.ReportError("decode UnionType", fmt.Sprintf("unknow type string %s in union %v", typeStr, e.union.Union.UnionType)) | |||||
| return | |||||
| } | |||||
| // 如果目标类型已经是个指针类型*T,那么在New的时候就需要使用T, | |||||
| // 否则New出来的是会是**T,这将导致后续的反序列化出问题 | |||||
| if typ.Kind() == reflect.Pointer { | |||||
| val := reflect.New(typ.Elem()) | |||||
| iter.ReadVal(val.Interface()) | |||||
| retVal := reflect.NewAt(e.union.Union.UnionType, ptr) | |||||
| retVal.Elem().Set(val) | |||||
| } else { | |||||
| val := reflect.New(typ) | |||||
| iter.ReadVal(val.Interface()) | |||||
| retVal := reflect.NewAt(e.union.Union.UnionType, ptr) | |||||
| retVal.Elem().Set(val.Elem()) | |||||
| } | |||||
| if iter.ReadObject() != "" { | |||||
| iter.ReportError("decode UnionType", "there should be only one fields in the json object") | |||||
| } | |||||
| } | |||||
| func makeDerefFullTypeName(typ reflect.Type) string { | |||||
| realType := typ | |||||
| for realType.Kind() == reflect.Pointer { | |||||
| realType = realType.Elem() | |||||
| } | |||||
| return fmt.Sprintf("%s.%s", realType.PkgPath(), realType.Name()) | |||||
| } | |||||
| @@ -4,9 +4,11 @@ import ( | |||||
| "fmt" | "fmt" | ||||
| "strconv" | "strconv" | ||||
| "time" | "time" | ||||
| "gitlink.org.cn/cloudream/common/utils/serder/types" | |||||
| ) | ) | ||||
| type Metadata struct{} | |||||
| type Metadata = types.Metadata | |||||
| type TimestampSecond time.Time | type TimestampSecond time.Time | ||||
| @@ -0,0 +1,3 @@ | |||||
| package types | |||||
| type Metadata struct{} | |||||