Skip to content

Commit f351829

Browse files
authored
Merge pull request #13 from dmcgowan/simplify-graph
Update graph logic and add further tests
2 parents d120f94 + e8139c6 commit f351829

File tree

2 files changed

+179
-19
lines changed

2 files changed

+179
-19
lines changed

plugin.go

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ var (
3939
ErrPluginNotFound = errors.New("plugin: not found")
4040
// ErrPluginMultipleInstances is used when a plugin is expected a single instance but has multiple
4141
ErrPluginMultipleInstances = errors.New("plugin: multiple instances")
42+
// ErrPluginCircularDependency is used when the graph detect a circular plugin dependency
43+
ErrPluginCircularDependency = errors.New("plugin: dependency loop detected")
4244

4345
// ErrInvalidRequires will be thrown if the requirements for a plugin are
4446
// defined in an invalid manner.
@@ -110,36 +112,43 @@ type Registry []*Registration
110112
// Graph computes the ordered list of registrations based on their dependencies,
111113
// filtering out any plugins which match the provided filter.
112114
func (registry Registry) Graph(filter DisableFilter) []Registration {
113-
disabled := map[*Registration]bool{}
114-
for _, r := range registry {
115-
if filter(r) {
116-
disabled[r] = true
115+
handled := make(map[*Registration]struct{}, len(registry))
116+
if filter != nil {
117+
for _, r := range registry {
118+
if filter(r) {
119+
handled[r] = struct{}{}
120+
}
117121
}
118122
}
119123

120-
ordered := make([]Registration, 0, len(registry)-len(disabled))
121-
added := map[*Registration]bool{}
124+
ordered := make([]Registration, 0, len(registry)-len(handled))
125+
stack := make([]*Registration, 0, cap(ordered))
122126
for _, r := range registry {
123-
if disabled[r] {
127+
if _, ok := handled[r]; ok {
124128
continue
125129
}
126-
children(r, registry, added, disabled, &ordered)
127-
if !added[r] {
128-
ordered = append(ordered, *r)
129-
added[r] = true
130-
}
130+
children(append(stack, r), registry, handled, &ordered)
131+
handled[r] = struct{}{}
132+
ordered = append(ordered, *r)
131133
}
132134
return ordered
133135
}
134136

135-
func children(reg *Registration, registry []*Registration, added, disabled map[*Registration]bool, ordered *[]Registration) {
137+
func children(stack []*Registration, registry []*Registration, handled map[*Registration]struct{}, ordered *[]Registration) {
138+
reg := stack[len(stack)-1]
136139
for _, t := range reg.Requires {
137140
for _, r := range registry {
138-
if (t == "*" || r.Type == t) && r != reg && !disabled[r] {
139-
children(r, registry, added, disabled, ordered)
140-
if !added[r] {
141+
if (t == "*" || r.Type == t) && r != reg {
142+
if _, ok := handled[r]; !ok {
143+
// Ensure not in current stack
144+
for _, p := range stack[:len(stack)-1] {
145+
if p == r {
146+
panic(fmt.Errorf("circular plugin dependency at %s: %w", r.URI(), ErrPluginCircularDependency))
147+
}
148+
}
149+
children(append(stack, r), registry, handled, ordered)
150+
handled[r] = struct{}{}
141151
*ordered = append(*ordered, *r)
142-
added[r] = true
143152
}
144153
}
145154
}
@@ -160,7 +169,7 @@ func (registry Registry) Register(r *Registration) Registry {
160169
}
161170

162171
for _, requires := range r.Requires {
163-
if requires == "*" && len(r.Requires) != 1 {
172+
if (requires == "*" && len(r.Requires) != 1) || requires == r.Type {
164173
panic(ErrInvalidRequires)
165174
}
166175
}

plugin_test.go

Lines changed: 152 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ const (
4444
)
4545

4646
func testRegistry() Registry {
47-
4847
var register Registry
4948
return register.Register(&Registration{
5049
Type: TaskMonitorPlugin,
@@ -517,6 +516,158 @@ func testPlugin(t Type, id string, i interface{}, err error) *Plugin {
517516
}
518517
}
519518

519+
func TestRequiresAll(t *testing.T) {
520+
var register Registry
521+
register = register.Register(&Registration{
522+
Type: InternalPlugin,
523+
ID: "system",
524+
}).Register(&Registration{
525+
Type: ServicePlugin,
526+
ID: "introspection",
527+
Requires: []Type{
528+
"*",
529+
},
530+
}).Register(&Registration{
531+
Type: ServicePlugin,
532+
ID: "task",
533+
Requires: []Type{
534+
InternalPlugin,
535+
},
536+
}).Register(&Registration{
537+
Type: ServicePlugin,
538+
ID: "version",
539+
})
540+
ordered := register.Graph(mockPluginFilter)
541+
expectedURI := []string{
542+
"io.containerd.internal.v1.system",
543+
"io.containerd.service.v1.task",
544+
"io.containerd.service.v1.version",
545+
"io.containerd.service.v1.introspection",
546+
}
547+
cmpOrdered(t, ordered, expectedURI)
548+
}
549+
550+
func TestRegisterErrors(t *testing.T) {
551+
552+
for _, tc := range []struct {
553+
name string
554+
expected error
555+
register func(Registry) Registry
556+
}{
557+
{
558+
name: "duplicate",
559+
expected: ErrIDRegistered,
560+
register: func(r Registry) Registry {
561+
return r.Register(&Registration{
562+
Type: TaskMonitorPlugin,
563+
ID: "cgroups",
564+
}).Register(&Registration{
565+
Type: TaskMonitorPlugin,
566+
ID: "cgroups",
567+
})
568+
},
569+
},
570+
{
571+
name: "circular",
572+
expected: ErrPluginCircularDependency,
573+
register: func(r Registry) Registry {
574+
// Circular dependencies should not loop but order will be based on registration order
575+
return r.Register(&Registration{
576+
Type: InternalPlugin,
577+
ID: "p1",
578+
Requires: []Type{
579+
RuntimePlugin,
580+
},
581+
}).Register(&Registration{
582+
Type: RuntimePlugin,
583+
ID: "p2",
584+
Requires: []Type{
585+
InternalPlugin,
586+
},
587+
}).Register(&Registration{
588+
Type: InternalPlugin,
589+
ID: "p3",
590+
})
591+
},
592+
},
593+
{
594+
name: "self",
595+
expected: ErrInvalidRequires,
596+
register: func(r Registry) Registry {
597+
// Circular dependencies should not loop but order will be based on registration order
598+
return r.Register(&Registration{
599+
Type: InternalPlugin,
600+
ID: "p1",
601+
Requires: []Type{
602+
InternalPlugin,
603+
},
604+
})
605+
},
606+
},
607+
{
608+
name: "no-type",
609+
expected: ErrNoType,
610+
register: func(r Registry) Registry {
611+
// Circular dependencies should not loop but order will be based on registration order
612+
return r.Register(&Registration{
613+
Type: "",
614+
ID: "p1",
615+
})
616+
},
617+
},
618+
{
619+
name: "no-ID",
620+
expected: ErrNoPluginID,
621+
register: func(r Registry) Registry {
622+
// Circular dependencies should not loop but order will be based on registration order
623+
return r.Register(&Registration{
624+
Type: InternalPlugin,
625+
ID: "",
626+
})
627+
},
628+
},
629+
{
630+
name: "bad-requires-all",
631+
expected: ErrInvalidRequires,
632+
register: func(r Registry) Registry {
633+
// Circular dependencies should not loop but order will be based on registration order
634+
return r.Register(&Registration{
635+
Type: InternalPlugin,
636+
ID: "p1",
637+
Requires: []Type{
638+
"*",
639+
InternalPlugin,
640+
},
641+
})
642+
},
643+
},
644+
} {
645+
t.Run(tc.name, func(t *testing.T) {
646+
var (
647+
r Registry
648+
panicAny any
649+
)
650+
func() {
651+
defer func() {
652+
panicAny = recover()
653+
}()
654+
655+
tc.register(r).Graph(mockPluginFilter)
656+
}()
657+
if panicAny == nil {
658+
t.Fatalf("expected panic with error %v", tc.expected)
659+
}
660+
err, ok := panicAny.(error)
661+
if !ok {
662+
t.Fatalf("expected panic: %v, expected error %v", panicAny, tc.expected)
663+
}
664+
if !errors.Is(err, tc.expected) {
665+
t.Fatalf("unexpected error type: %v, expected %v", panicAny, tc.expected)
666+
}
667+
})
668+
}
669+
}
670+
520671
func BenchmarkGraph(b *testing.B) {
521672
register := testRegistry()
522673
b.ResetTimer()

0 commit comments

Comments
 (0)