Skip to content

Commit

Permalink
feat: add function runtime to dig.CallbackInfo (#412)
Browse files Browse the repository at this point in the history
This change adds runtime of the associated constructor
or decorator to dig.CallbackInfo.

For example, users can access the runtime of a particular
constructor by:
```go
c := dig.New()
c.Provide(NewFoo, dig.WithProviderCallback(func(ci dig.CallbackInfo) {
    if ci.Error == nil {
        fmt.Printf("constructor %q finished running in %v", ci.Name, ci.Runtime)
    }
}))
```
This change is a prerequisite for adding uber-go/fx#1213
to report runtime of constructors in Run events.
  • Loading branch information
tchung1118 authored Jul 2, 2024
1 parent 11da7b7 commit 89f5733
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 5 deletions.
6 changes: 6 additions & 0 deletions callback.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

package dig

import "time"

// CallbackInfo contains information about a provided function or decorator
// called by Dig, and is passed to a [Callback] registered with
// [WithProviderCallback] or [WithDecoratorCallback].
Expand All @@ -32,6 +34,10 @@ type CallbackInfo struct {
// function, if any. When used in conjunction with [RecoverFromPanics],
// this will be set to a [PanicError] when the function panics.
Error error

// Runtime contains the duration it took for the associated
// function to run.
Runtime time.Duration
}

// Callback is a function that can be registered with a provided function
Expand Down
6 changes: 4 additions & 2 deletions constructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,13 @@ func (n *constructorNode) Call(c containerStore) (err error) {
}

if n.callback != nil {
start := c.clock().Now()
// Wrap in separate func to include PanicErrors
defer func() {
n.callback(CallbackInfo{
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
Error: err,
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
Error: err,
Runtime: c.clock().Since(start),
})
}()
}
Expand Down
19 changes: 19 additions & 0 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"math/rand"
"reflect"

"go.uber.org/dig/internal/digclock"
"go.uber.org/dig/internal/dot"
)

Expand Down Expand Up @@ -141,6 +142,9 @@ type containerStore interface {

// Returns invokerFn function to use when calling arguments.
invoker() invokerFn

// Returns a clock to use
clock() digclock.Clock
}

// New constructs a Container.
Expand Down Expand Up @@ -211,6 +215,21 @@ func (o setRandOption) applyOption(c *Container) {
c.scope.rand = o.r
}

// Changes the source of time for the container.
func setClock(c digclock.Clock) Option {
return setClockOption{c: c}
}

type setClockOption struct{ c digclock.Clock }

func (o setClockOption) String() string {
return fmt.Sprintf("setClock(%v)", o.c)
}

func (o setClockOption) applyOption(c *Container) {
c.scope.clockSrc = o.c
}

// DryRun is an Option which, when set to true, disables invocation of functions supplied to
// Provide and Invoke. Use this to build no-op containers.
func DryRun(dry bool) Option {
Expand Down
6 changes: 4 additions & 2 deletions decorate.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,11 +122,13 @@ func (n *decoratorNode) Call(s containerStore) (err error) {
}

if n.callback != nil {
start := s.clock().Now()
// Wrap in separate func to include PanicErrors
defer func() {
n.callback(CallbackInfo{
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
Error: err,
Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
Error: err,
Runtime: s.clock().Since(start),
})
}()
}
Expand Down
10 changes: 9 additions & 1 deletion dig_int_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,16 @@

package dig

import "math/rand"
import (
"math/rand"

"go.uber.org/dig/internal/digclock"
)

func SetRand(r *rand.Rand) Option {
return setRand(r)
}

func SetClock(c digclock.Clock) Option {
return setClock(c)
}
50 changes: 50 additions & 0 deletions dig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/dig"
"go.uber.org/dig/internal/digclock"
"go.uber.org/dig/internal/digtest"
)

Expand Down Expand Up @@ -1796,6 +1797,55 @@ func TestCallback(t *testing.T) {
})
}

func TestCallbackRuntime(t *testing.T) {
t.Run("constructor runtime", func(t *testing.T) {
var called bool

mockClock := digclock.NewMock()
c := digtest.New(t, dig.SetClock(mockClock))
c.RequireProvide(
func() int {
mockClock.Add(1 * time.Millisecond)
return 5
},
dig.WithProviderCallback(func(ci dig.CallbackInfo) {
assert.Equal(t, "go.uber.org/dig_test.TestCallbackRuntime.func1.1", ci.Name)
assert.NoError(t, ci.Error)
assert.Equal(t, ci.Runtime, 1*time.Millisecond)

called = true
}),
)

c.Invoke(func(int) {})
assert.True(t, called)
})

t.Run("decorator runtime", func(t *testing.T) {
var called bool

mockClock := digclock.NewMock()
c := digtest.New(t, dig.SetClock(mockClock))
c.RequireProvide(giveInt)
c.RequireDecorate(
func(int) int {
mockClock.Add(1 * time.Millisecond)
return 10
},
dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
assert.Equal(t, "go.uber.org/dig_test.TestCallbackRuntime.func2.1", ci.Name)
assert.NoError(t, ci.Error)
assert.Equal(t, ci.Runtime, 1*time.Millisecond)

called = true
}),
)

c.Invoke(func(int) {})
assert.True(t, called)
})
}

func TestProvideConstructorErrors(t *testing.T) {
t.Run("multiple-type constructor returns multiple objects of same type", func(t *testing.T) {
c := digtest.New(t)
Expand Down
82 changes: 82 additions & 0 deletions internal/digclock/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2024 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package digclock

import (
"time"
)

// Clock defines how dig accesses time.
type Clock interface {
Now() time.Time
Since(time.Time) time.Duration
}

// System is the default implementation of Clock based on real time.
var System Clock = systemClock{}

type systemClock struct{}

func (systemClock) Now() time.Time {
return time.Now()
}

func (systemClock) Since(t time.Time) time.Duration {
return time.Since(t)
}

// Mock is a fake source of time.
// It implements standard time operations, but allows
// the user to control the passage of time.
//
// Use the [Mock.Add] method to progress time.
//
// Note that this implementation is not safe for concurrent use.
type Mock struct {
now time.Time
}

var _ Clock = (*Mock)(nil)

// NewMock creates a new mock clock with the current time set to the current time.
func NewMock() *Mock {
return &Mock{now: time.Now()}
}

// Now returns the current time.
func (m *Mock) Now() time.Time {
return m.now
}

// Since returns the time elapsed since the given time.
func (m *Mock) Since(t time.Time) time.Duration {
return m.Now().Sub(t)
}

// Add progresses time by the given duration.
//
// It panics if the duration is negative.
func (m *Mock) Add(d time.Duration) {
if d < 0 {
panic("cannot add negative duration")
}
m.now = m.now.Add(d)
}
53 changes: 53 additions & 0 deletions internal/digclock/clock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) 2024 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package digclock

import (
"testing"
"time"

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

func TestSystemClock(t *testing.T) {
clock := System
testClock(t, clock, func(d time.Duration) { time.Sleep(d) })
}

func TestMockClock(t *testing.T) {
clock := NewMock()
testClock(t, clock, clock.Add)
}

func testClock(t *testing.T, clock Clock, advance func(d time.Duration)) {
now := clock.Now()
assert.False(t, now.IsZero())

t.Run("Since", func(t *testing.T) {
advance(1 * time.Millisecond)
assert.NotZero(t, clock.Since(now), "time must have advanced")
})
}

func TestMock_AddNegative(t *testing.T) {
clock := NewMock()
assert.Panics(t, func() { clock.Add(-1) })
}
11 changes: 11 additions & 0 deletions scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"reflect"
"sort"
"time"

"go.uber.org/dig/internal/digclock"
)

// A ScopeOption modifies the default behavior of Scope; currently,
Expand Down Expand Up @@ -90,6 +92,9 @@ type Scope struct {

// All the child scopes of this Scope.
childScopes []*Scope

// clockSrc stores the source of time. Defaults to system clock.
clockSrc digclock.Clock
}

func newScope() *Scope {
Expand All @@ -102,6 +107,7 @@ func newScope() *Scope {
decoratedGroups: make(map[key]reflect.Value),
invokerFn: defaultInvoker,
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
clockSrc: digclock.System,
}
s.gh = newGraphHolder(s)
return s
Expand All @@ -117,6 +123,7 @@ func (s *Scope) Scope(name string, opts ...ScopeOption) *Scope {
child.name = name
child.parentScope = s
child.invokerFn = s.invokerFn
child.clockSrc = s.clockSrc
child.deferAcyclicVerification = s.deferAcyclicVerification
child.recoverFromPanics = s.recoverFromPanics

Expand Down Expand Up @@ -267,6 +274,10 @@ func (s *Scope) invoker() invokerFn {
return s.invokerFn
}

func (s *Scope) clock() digclock.Clock {
return s.clockSrc
}

// adds a new graphNode to this Scope and all of its descendent
// scope.
func (s *Scope) newGraphNode(wrapped interface{}, orders map[*Scope]int) {
Expand Down

0 comments on commit 89f5733

Please sign in to comment.