Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions injector.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ type Injector interface {
// ShutdownWithContext gracefully shuts down the injector and all its descendant scopes with context support.
ShutdownWithContext(context.Context) *ShutdownReport

// Delete gracefully shuts down the injector scope and removes it from its parent when applicable.
Delete() *ShutdownReport

// DeleteWithContext gracefully shuts down the injector scope with context support and removes it from its parent when applicable.
DeleteWithContext(context.Context) *ShutdownReport

// clone creates a deep copy of the injector with all its services and child scopes.
clone(*RootScope, *Scope) *Scope

Expand Down
17 changes: 17 additions & 0 deletions root_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,23 @@ func (s *RootScope) ShutdownWithContext(ctx context.Context) *ShutdownReport {
return s.self.ShutdownWithContext(ctx)
}

// Delete gracefully shuts down the root scope and removes it from the hierarchy.
// Since the root scope has no parent, this behaves like Shutdown.
func (s *RootScope) Delete() *ShutdownReport { return s.DeleteWithContext(context.Background()) }

// DeleteWithContext gracefully shuts down the root scope with context support and removes it from the hierarchy.
// This is equivalent to ShutdownWithContext but keeps API parity with Scope.
func (s *RootScope) DeleteWithContext(ctx context.Context) *ShutdownReport {
defer func() {
if s.healthCheckPool != nil {
s.healthCheckPool.stop()
s.healthCheckPool = nil
}
}()

return s.self.DeleteWithContext(ctx)
}

func (s *RootScope) clone(root *RootScope, parent *Scope) *Scope { return s.self.clone(root, parent) }
func (s *RootScope) serviceExist(name string) bool { return s.self.serviceExist(name) }
func (s *RootScope) serviceExistRec(name string) bool { return s.self.serviceExistRec(name) }
Expand Down
36 changes: 35 additions & 1 deletion scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type Scope struct {
name string // Human-readable name for the scope (immutable)
rootScope *RootScope // Reference to the root scope (immutable)
parentScope *Scope // Reference to the immediate parent scope (immutable)
childScopes map[string]*Scope // Map of child scopes (append only)
childScopes map[string]*Scope // Map of child scopes (managed under lock)

mu sync.RWMutex // Mutex for thread-safe operations
services map[string]any // Map of registered services
Expand Down Expand Up @@ -368,6 +368,40 @@ func (s *Scope) ShutdownWithContext(ctx context.Context) *ShutdownReport {
return report
}

// Delete gracefully shuts down the scope (including all descendants) and removes it from its parent scope.
// When called on the root scope, this behaves like ShutdownWithContext.
func (s *Scope) Delete() *ShutdownReport {
return s.DeleteWithContext(context.Background())
}

// DeleteWithContext gracefully shuts down the scope with context support and removes it from its parent scope.
// It ensures the scope hierarchy stays consistent after deletion.
func (s *Scope) DeleteWithContext(ctx context.Context) *ShutdownReport {
s.logf("requested delete")

parent := s.parentScope

report := s.ShutdownWithContext(ctx)

if parent != nil {
parent.mu.Lock()
for name, scope := range parent.childScopes {
if scope == s {
delete(parent.childScopes, name)
break
}
}
parent.mu.Unlock()
s.logf("delete completed; detached from parent `%s`", parent.name)
} else if s.rootScope != nil && s.rootScope.self == s {
s.logf("delete completed on root scope")
} else {
s.logf("delete completed on scope without parent")
}

return report
}

// shutdownChildrenInParallel runs a parallel shutdown of children scopes.
// This method shuts down all child scopes concurrently and then removes them
// from the scope hierarchy.
Expand Down
31 changes: 31 additions & 0 deletions scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,37 @@ func TestScope_Scope(t *testing.T) {
is.Equal(0, child2.orderedInvocationIndex)
}

func TestScope_Delete(t *testing.T) {
t.Parallel()
testWithTimeout(t, 300*time.Millisecond)
is := assert.New(t)

root := New()
child := root.Scope("child")
grandChild := child.Scope("grandchild")

ProvideNamedValue(child, "child-value", 42)
ProvideNamedValue(grandChild, "grandchild-value", "hello")

val, err := InvokeNamed[int](child, "child-value")
is.NoError(err)
is.Equal(42, val)

report := child.Delete()
is.True(report.Succeed)

_, ok := root.ChildByName("child")
is.False(ok)

_, err = InvokeNamed[int](child, "child-value")
is.Error(err)
_, err = InvokeNamed[string](grandChild, "grandchild-value")
is.Error(err)

child2 := root.Scope("child")
is.NotNil(child2)
}

func TestScope_Scope_race(t *testing.T) {
testWithTimeout(t, 300*time.Millisecond)
injector := New()
Expand Down
4 changes: 4 additions & 0 deletions virtual_scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func (s *virtualScope) Shutdown() *ShutdownReport { return s.self.Shutdown() }
func (s *virtualScope) ShutdownWithContext(ctx context.Context) *ShutdownReport {
return s.self.ShutdownWithContext(ctx)
}
func (s *virtualScope) Delete() *ShutdownReport { return s.self.Delete() }
func (s *virtualScope) DeleteWithContext(ctx context.Context) *ShutdownReport {
return s.self.DeleteWithContext(ctx)
}
func (s *virtualScope) clone(r *RootScope, p *Scope) *Scope { return s.self.clone(r, p) }
func (s *virtualScope) serviceExist(name string) bool { return s.self.serviceExist(name) }
func (s *virtualScope) serviceExistRec(name string) bool { return s.self.serviceExistRec(name) }
Expand Down