Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚠️ Fix some more races in the delegating logger #1361

Merged
merged 1 commit into from Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
100 changes: 85 additions & 15 deletions pkg/log/deleg.go
Expand Up @@ -29,11 +29,11 @@ type loggerPromise struct {
childPromises []*loggerPromise
promisesLock sync.Mutex

name *string
tags []interface{}
name *string
tags []interface{}
level int
}

// WithName provides a new Logger with the name appended
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing godocs?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it doesn't matter because its an interface method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err yeah sorry, I abused this to check if the linter correctly doesn't care about godocs of exported methods on unexported types (which it does). Since it is only visible from within the package it doesn't matter much IMO

func (p *loggerPromise) WithName(l *DelegatingLogger, name string) *loggerPromise {
res := &loggerPromise{
logger: l,
Expand Down Expand Up @@ -61,6 +61,19 @@ func (p *loggerPromise) WithValues(l *DelegatingLogger, tags ...interface{}) *lo
return res
}

func (p *loggerPromise) V(l *DelegatingLogger, level int) *loggerPromise {
res := &loggerPromise{
logger: l,
level: level,
promisesLock: sync.Mutex{},
}

p.promisesLock.Lock()
defer p.promisesLock.Unlock()
p.childPromises = append(p.childPromises, res)
return res
}

// Fulfill instantiates the Logger with the provided logger
func (p *loggerPromise) Fulfill(parentLogger logr.Logger) {
var logger = parentLogger
Expand All @@ -71,9 +84,12 @@ func (p *loggerPromise) Fulfill(parentLogger logr.Logger) {
if p.tags != nil {
logger = logger.WithValues(p.tags...)
}
if p.level != 0 {
logger = logger.V(p.level)
}

p.logger.lock.Lock()
p.logger.Logger = logger
p.logger.logger = logger
p.logger.promise = nil
p.logger.lock.Unlock()

Expand All @@ -88,21 +104,75 @@ func (p *loggerPromise) Fulfill(parentLogger logr.Logger) {
// logger. It expects to have *some* logr.Logger set at all times (generally
// a no-op logger before the promises are fulfilled).
type DelegatingLogger struct {
lock sync.Mutex
logr.Logger
lock sync.RWMutex
logger logr.Logger
promise *loggerPromise
}

// Enabled tests whether this Logger is enabled. For example, commandline
// flags might be used to set the logging verbosity and disable some info
// logs.
func (l *DelegatingLogger) Enabled() bool {
l.lock.RLock()
defer l.lock.RUnlock()
return l.logger.Enabled()
}

// Info logs a non-error message with the given key/value pairs as context.
//
// The msg argument should be used to add some constant description to
// the log line. The key/value pairs can then be used to add additional
// variable information. The key/value pairs should alternate string
// keys and arbitrary values.
func (l *DelegatingLogger) Info(msg string, keysAndValues ...interface{}) {
l.lock.RLock()
defer l.lock.RUnlock()
Comment on lines +128 to +129
Copy link
Contributor

@estroz estroz Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't these be write locks? Isn't Info writing to the underlying logger here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Lock protects the reference to the logger, not the method call. The actual logger implementation must also be threadsafe, but that needs to be done in that implementation, not here. Everything except Fullfill only reads the reference (as proven by the included tests not failing with a data race)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we care about that, I can update the PR later to only use the lock to get the logger reference and not around the whole method call, which might improve performance slightly

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Up to you.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then I'll keep it as-is, only protecting the reference would require to allocate a pointer so it might actually be worse performance-wise and is more complex

l.logger.Info(msg, keysAndValues...)
}

// Error logs an error, with the given message and key/value pairs as context.
// It functions similarly to calling Info with the "error" named value, but may
// have unique behavior, and should be preferred for logging errors (see the
// package documentations for more information).
//
// The msg field should be used to add context to any underlying error,
// while the err field should be used to attach the actual error that
// triggered this log line, if present.
func (l *DelegatingLogger) Error(err error, msg string, keysAndValues ...interface{}) {
l.lock.RLock()
defer l.lock.RUnlock()
l.logger.Error(err, msg, keysAndValues...)
}

// V returns an Logger value for a specific verbosity level, relative to
// this Logger. In other words, V values are additive. V higher verbosity
// level means a log message is less important. It's illegal to pass a log
// level less than zero.
func (l *DelegatingLogger) V(level int) logr.Logger {
l.lock.RLock()
defer l.lock.RUnlock()

if l.promise == nil {
return l.logger.V(level)
}

res := &DelegatingLogger{logger: l.logger}
promise := l.promise.V(res, level)
res.promise = promise

return res
}

// WithName provides a new Logger with the name appended
func (l *DelegatingLogger) WithName(name string) logr.Logger {
l.lock.Lock()
defer l.lock.Unlock()
l.lock.RLock()
defer l.lock.RUnlock()

if l.promise == nil {
return l.Logger.WithName(name)
return l.logger.WithName(name)
}

res := &DelegatingLogger{Logger: l.Logger}
res := &DelegatingLogger{logger: l.logger}
promise := l.promise.WithName(res, name)
res.promise = promise

Expand All @@ -111,14 +181,14 @@ func (l *DelegatingLogger) WithName(name string) logr.Logger {

// WithValues provides a new Logger with the tags appended
func (l *DelegatingLogger) WithValues(tags ...interface{}) logr.Logger {
l.lock.Lock()
defer l.lock.Unlock()
l.lock.RLock()
defer l.lock.RUnlock()

if l.promise == nil {
return l.Logger.WithValues(tags...)
return l.logger.WithValues(tags...)
}

res := &DelegatingLogger{Logger: l.Logger}
res := &DelegatingLogger{logger: l.logger}
promise := l.promise.WithValues(res, tags...)
res.promise = promise

Expand All @@ -138,7 +208,7 @@ func (l *DelegatingLogger) Fulfill(actual logr.Logger) {
// the given logger before it's promise is fulfilled.
func NewDelegatingLogger(initial logr.Logger) *DelegatingLogger {
l := &DelegatingLogger{
Logger: initial,
logger: initial,
promise: &loggerPromise{promisesLock: sync.Mutex{}},
}
l.promise.logger = l
Expand Down
31 changes: 31 additions & 0 deletions pkg/log/log_test.go
Expand Up @@ -18,12 +18,15 @@ package log

import (
"context"
"errors"

"github.com/go-logr/logr"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ logr.Logger = &DelegatingLogger{}

// logInfo is the information for a particular fakeLogger message
type logInfo struct {
name []string
Expand Down Expand Up @@ -169,6 +172,10 @@ var _ = Describe("logging", func() {
withNameDone := make(chan struct{})
withValuesDone := make(chan struct{})
grandChildDone := make(chan struct{})
logEnabledDone := make(chan struct{})
logInfoDone := make(chan struct{})
logErrorDone := make(chan struct{})
logVDone := make(chan struct{})

// Constructing the child in the goroutine does not reliably
// trigger the race detector
Expand All @@ -193,11 +200,35 @@ var _ = Describe("logging", func() {
child.WithValues("grandchild")
close(grandChildDone)
}()
go func() {
defer GinkgoRecover()
delegLog.Enabled()
close(logEnabledDone)
}()
go func() {
defer GinkgoRecover()
delegLog.Info("hello world")
close(logInfoDone)
}()
go func() {
defer GinkgoRecover()
delegLog.Error(errors.New("err"), "hello world")
close(logErrorDone)
}()
go func() {
defer GinkgoRecover()
delegLog.V(1)
close(logVDone)
}()

<-fulfillDone
<-withNameDone
<-withValuesDone
<-grandChildDone
<-logEnabledDone
<-logInfoDone
<-logErrorDone
<-logVDone
})

It("should delegate with tags", func() {
Expand Down