Skip to content

Commit

Permalink
feat: better circular dependency detection
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelsmejkal committed May 2, 2024
1 parent e1af41d commit 68aaa62
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 19 deletions.
53 changes: 35 additions & 18 deletions plumber.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,41 @@ import (

// Errors
var (
// ErrResolving error indicating circular dependency
ErrResolving = errors.New("just resolving, possible circular dependency")
// ErrCircularDependency error indicating circular dependency
ErrCircularDependency = errors.New("circular dependency")
)

// Dependency represent a dependency that can be supplied into Require method
type Dependency interface {
Iterate(func(dep Dependency) bool)
Error() error
}

// Future represents a struct that will help with dependency evaluation
type Future[T any] struct {
deps []Dependency
d *D[T]
d *D[T]
}

// Then evaluates a dependencies and trigger callback when all good
func (f *Future[T]) Then(callback func()) {
var errs []error
for _, d := range f.deps {
err := d.Resolved()
for _, d := range f.d.deps {
var (
circular bool
err error
)
d.Iterate(func(dep Dependency) bool {
if f.d == dep {
circular = true
}
return !circular
})
if circular {
err = ErrCircularDependency
}
if err == nil {
err = d.Error()
}
if err != nil {
errs = append(errs, fmt.Errorf("dependency not resolved, %s requires %s (%w)", f.d, d, err))
}
Expand All @@ -51,6 +71,7 @@ type D[T any] struct {
once sync.Once
mx sync.Mutex
resolve func()
deps []Dependency
}

// String return names of underlaying type
Expand Down Expand Up @@ -144,12 +165,13 @@ func (d *D[T]) Error() error {

// Resolved return true if current value was resolved and is valid
// In case that current value is just being resolved it return false to not trigger infinite loop during cyclic dependency
func (d *D[T]) Resolved() error {
if d.resolving {
return ErrResolving
func (d *D[T]) Iterate(callback func(dep Dependency) bool) {
for _, dep := range d.deps {
if !callback(dep) {
break
}
dep.Iterate(callback)
}
_, err := d.InstanceError()
return err
}

// Resolve returns a callback providing a resolution orchestrator
Expand Down Expand Up @@ -232,9 +254,9 @@ func (r *Resolution[T]) ResolveError(v T, err error) {
// Require allows to define a dependant for the current value
// It is a necessary to call Then to trigger a dependency evaluation
func (r *Resolution[T]) Require(deps ...Dependency) *Future[T] {
r.d.deps = deps
return &Future[T]{
d: r.d,
deps: deps,
d: r.d,
}
}

Expand Down Expand Up @@ -268,8 +290,3 @@ func (rr *ResolutionR[T]) ResolveAdapter(v T, runnable RunnerCloser) {
func (rr *ResolutionR[T]) Require(deps ...Dependency) *Future[T] {
return rr.resolution.Require(deps...)
}

// Dependency represent a dependency that can be supplied into Require method
type Dependency interface {
Resolved() error
}
36 changes: 35 additions & 1 deletion plumber_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"net/http"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -127,7 +128,40 @@ func TestRequireNotOkCycle(t *testing.T) {
})
v, err := a.D2.InstanceError()
assert.Equal(t, v, 0)
assert.Error(t, err, "dependency not resolved, int requires int, just resolving, possible circular dependency")
assert.Error(t, err, "dependency not resolved, int requires int (circular dependency)")
}

func TestRequireConcurrentOk(t *testing.T) {
type concurrent struct{}
a := struct {
D1 plumber.D[int]
D2 plumber.D[int]
Concurrent plumber.D[concurrent]
}{}
a.D1.Const(1)
a.D2.Resolve(func(r *plumber.Resolution[int]) {
r.Require(&a.D1, &a.Concurrent).Then(func() {
r.Resolve(1)
})
})
a.Concurrent.Define(func() concurrent {
time.Sleep(100 * time.Millisecond)
return concurrent{}
})
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
v, err := a.D2.InstanceError()
assert.NilError(t, err)
assert.Equal(t, v, 1)
}()
go func() {
defer wg.Done()
_, err := a.Concurrent.InstanceError()
assert.NilError(t, err)
}()
wg.Wait()
}

func TestExamplePipeline(t *testing.T) {
Expand Down

0 comments on commit 68aaa62

Please sign in to comment.