Skip to content

Commit

Permalink
fn/ContextGuard: use context.AfterFunc to wait
Browse files Browse the repository at this point in the history
Simplifies context cancellation handling by using context.AfterFunc instead of a
goroutine to wait for context cancellation. This approach avoids the overhead of
a goroutine during the waiting period.

For ctxQuitUnsafe, since g.quit is closed only in the Quit method (which also
cancels all associated contexts), waiting on context cancellation ensures the
same behavior without unnecessary dependency on g.quit.

Added a test to ensure that the Create method does not launch any goroutines.
  • Loading branch information
starius committed Dec 14, 2024
1 parent 334c7be commit b22cc4f
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 20 deletions.
37 changes: 17 additions & 20 deletions fn/context_guard.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,9 +173,10 @@ func (g *ContextGuard) Create(ctx context.Context,
return ctx, cancel
}

// ctxQuitUnsafe spins off a goroutine that will block until the passed context
// is cancelled or until the quit channel has been signaled after which it will
// call the passed cancel function and decrement the wait group.
// ctxQuitUnsafe increases the wait group counter, waits until the context is
// cancelled and decreases the wait group counter. It stores the passed cancel
// function and returns a wrapped version, which removed the stored one and
// calls it. Quit method calls all the stored cancel functions.
//
// NOTE: the caller must hold the ContextGuard's mutex before calling this
// function.
Expand All @@ -185,31 +186,27 @@ func (g *ContextGuard) ctxQuitUnsafe(ctx context.Context,
cancel = g.addCancelFnUnsafe(cancel)

g.wg.Add(1)
go func() {
defer cancel()
defer g.wg.Done()

select {
case <-g.quit:

case <-ctx.Done():
}
}()
// We don't have to wait on g.quit here: g.quit can be closed only in
// Quit method, which also closes the context we are waiting for.
context.AfterFunc(ctx, func() {
g.wg.Done()
})

return cancel
}

// ctxBlocking spins off a goroutine that will block until the passed context
// is cancelled after which it will decrement the wait group.
// ctxBlocking increases the wait group counter, waits until the context is
// cancelled and decreases the wait group counter.
//
// NOTE: the caller must hold the ContextGuard's mutex before calling this
// function.
func (g *ContextGuard) ctxBlocking(ctx context.Context) {
g.wg.Add(1)
go func() {
defer g.wg.Done()

select {
case <-ctx.Done():
}
}()
context.AfterFunc(ctx, func() {
g.wg.Done()
})
}

// addCancelFnUnsafe adds a context cancel function to the manager and returns a
Expand Down
32 changes: 32 additions & 0 deletions fn/context_guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package fn

import (
"context"
"runtime"
"testing"
"time"

"github.com/stretchr/testify/require"
)

// TestContextGuard tests the behaviour of the ContextGuard.
Expand Down Expand Up @@ -439,3 +442,32 @@ func TestContextGuard(t *testing.T) {
}
})
}

// TestContextGuardCountGoroutines makes sure that ContextGuard doesn't create
// any goroutines while waiting for contexts.
func TestContextGuardCountGoroutines(t *testing.T) {
g := NewContextGuard()

ctx, cancel := context.WithCancel(context.Background())

// Count goroutines before contexts are created.
count1 := runtime.NumGoroutine()

// Create 1000 contexts of each type.
for i := 0; i < 1000; i++ {
_, _ = g.Create(ctx)
_, _ = g.Create(ctx, WithBlockingCG())
_, _ = g.Create(ctx, WithTimeoutCG())
_, _ = g.Create(ctx, WithBlockingCG(), WithTimeoutCG())
}

// Make sure no new goroutine was launched.
count2 := runtime.NumGoroutine()
require.LessOrEqual(t, count2, count1)

// Cancel root context.
cancel()

// Make sure wg's counter gets to 0 eventually.
g.WgWait()
}

0 comments on commit b22cc4f

Please sign in to comment.