Skip to content

Commit 2327d4c

Browse files
committed
data race: avoid unprotected access to sb.file
79575d8 (included in klog v1.130.0) added a small data race with regards to accessing the syncBuffer.file field when calling Sync. We can eliminate the entire redirectBuffer interface that led to this mistake and instead work directly with syncBuffer for flushing and syncing. To avoid allocating a slice inside flushAll, a fixed-sized struct is returned. A benchmark confirms that flushAll+syncAll can be called without allocations.
1 parent 16c7d26 commit 2327d4c

File tree

2 files changed

+81
-61
lines changed

2 files changed

+81
-61
lines changed

klog.go

+25-52
Original file line numberDiff line numberDiff line change
@@ -404,13 +404,6 @@ func (t *traceLocation) Set(value string) error {
404404
return nil
405405
}
406406

407-
// flushSyncWriter is the interface satisfied by logging destinations.
408-
type flushSyncWriter interface {
409-
Flush() error
410-
Sync() error
411-
io.Writer
412-
}
413-
414407
var logging loggingT
415408
var commandLine flag.FlagSet
416409

@@ -486,7 +479,7 @@ type settings struct {
486479
// Access to all of the following fields must be protected via a mutex.
487480

488481
// file holds writer for each of the log types.
489-
file [severity.NumSeverity]flushSyncWriter
482+
file [severity.NumSeverity]io.Writer
490483
// flushInterval is the interval for periodic flushing. If zero,
491484
// the global default will be used.
492485
flushInterval time.Duration
@@ -831,32 +824,12 @@ func (l *loggingT) printS(err error, s severity.Severity, depth int, msg string,
831824
buffer.PutBuffer(b)
832825
}
833826

834-
// redirectBuffer is used to set an alternate destination for the logs
835-
type redirectBuffer struct {
836-
w io.Writer
837-
}
838-
839-
func (rb *redirectBuffer) Sync() error {
840-
return nil
841-
}
842-
843-
func (rb *redirectBuffer) Flush() error {
844-
return nil
845-
}
846-
847-
func (rb *redirectBuffer) Write(bytes []byte) (n int, err error) {
848-
return rb.w.Write(bytes)
849-
}
850-
851827
// SetOutput sets the output destination for all severities
852828
func SetOutput(w io.Writer) {
853829
logging.mu.Lock()
854830
defer logging.mu.Unlock()
855831
for s := severity.FatalLog; s >= severity.InfoLog; s-- {
856-
rb := &redirectBuffer{
857-
w: w,
858-
}
859-
logging.file[s] = rb
832+
logging.file[s] = w
860833
}
861834
}
862835

@@ -868,10 +841,7 @@ func SetOutputBySeverity(name string, w io.Writer) {
868841
if !ok {
869842
panic(fmt.Sprintf("SetOutputBySeverity(%q): unrecognized severity name", name))
870843
}
871-
rb := &redirectBuffer{
872-
w: w,
873-
}
874-
logging.file[sev] = rb
844+
logging.file[sev] = w
875845
}
876846

877847
// LogToStderr sets whether to log exclusively to stderr, bypassing outputs
@@ -1011,8 +981,8 @@ func (l *loggingT) exit(err error) {
1011981
logExitFunc(err)
1012982
return
1013983
}
1014-
files := l.flushAll()
1015-
l.syncAll(files)
984+
needToSync := l.flushAll()
985+
l.syncAll(needToSync)
1016986
OsExit(2)
1017987
}
1018988

@@ -1029,10 +999,6 @@ type syncBuffer struct {
1029999
maxbytes uint64 // The max number of bytes this syncBuffer.file can hold before cleaning up.
10301000
}
10311001

1032-
func (sb *syncBuffer) Sync() error {
1033-
return sb.file.Sync()
1034-
}
1035-
10361002
// CalculateMaxSize returns the real max size in bytes after considering the default max size and the flag options.
10371003
func CalculateMaxSize() uint64 {
10381004
if logging.logFile != "" {
@@ -1224,37 +1190,44 @@ func StartFlushDaemon(interval time.Duration) {
12241190
// lockAndFlushAll is like flushAll but locks l.mu first.
12251191
func (l *loggingT) lockAndFlushAll() {
12261192
l.mu.Lock()
1227-
files := l.flushAll()
1193+
needToSync := l.flushAll()
12281194
l.mu.Unlock()
12291195
// Some environments are slow when syncing and holding the lock might cause contention.
1230-
l.syncAll(files)
1196+
l.syncAll(needToSync)
12311197
}
12321198

12331199
// flushAll flushes all the logs
12341200
// l.mu is held.
1235-
func (l *loggingT) flushAll() []flushSyncWriter {
1236-
files := make([]flushSyncWriter, 0, severity.NumSeverity)
1201+
//
1202+
// The result is the number of files which need to be synced and the pointers to them.
1203+
func (l *loggingT) flushAll() fileArray {
1204+
var needToSync fileArray
1205+
12371206
// Flush from fatal down, in case there's trouble flushing.
12381207
for s := severity.FatalLog; s >= severity.InfoLog; s-- {
12391208
file := l.file[s]
1240-
if file != nil {
1241-
_ = file.Flush() // ignore error
1209+
if sb, ok := file.(*syncBuffer); ok && sb.file != nil {
1210+
_ = sb.Flush() // ignore error
1211+
needToSync.files[needToSync.num] = sb.file
1212+
needToSync.num++
12421213
}
1243-
files = append(files, file)
12441214
}
12451215
if logging.loggerOptions.flush != nil {
12461216
logging.loggerOptions.flush()
12471217
}
1248-
return files
1218+
return needToSync
1219+
}
1220+
1221+
type fileArray struct {
1222+
num int
1223+
files [severity.NumSeverity]*os.File
12491224
}
12501225

12511226
// syncAll attempts to "sync" their data to disk.
1252-
func (l *loggingT) syncAll(files []flushSyncWriter) {
1227+
func (l *loggingT) syncAll(needToSync fileArray) {
12531228
// Flush from fatal down, in case there's trouble flushing.
1254-
for _, file := range files {
1255-
if file != nil {
1256-
_ = file.Sync() // ignore error
1257-
}
1229+
for i := 0; i < needToSync.num; i++ {
1230+
_ = needToSync.files[i].Sync() // ignore error
12581231
}
12591232
}
12601233

klog_test.go

+56-9
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"errors"
2222
"flag"
2323
"fmt"
24+
"io"
2425
"io/ioutil"
2526
stdLog "log"
2627
"os"
@@ -73,7 +74,7 @@ func (f *flushBuffer) Sync() error {
7374
}
7475

7576
// swap sets the log writers and returns the old array.
76-
func (l *loggingT) swap(writers [severity.NumSeverity]flushSyncWriter) (old [severity.NumSeverity]flushSyncWriter) {
77+
func (l *loggingT) swap(writers [severity.NumSeverity]io.Writer) (old [severity.NumSeverity]io.Writer) {
7778
l.mu.Lock()
7879
defer l.mu.Unlock()
7980
old = l.file
@@ -82,8 +83,8 @@ func (l *loggingT) swap(writers [severity.NumSeverity]flushSyncWriter) (old [sev
8283
}
8384

8485
// newBuffers sets the log writers to all new byte buffers and returns the old array.
85-
func (l *loggingT) newBuffers() [severity.NumSeverity]flushSyncWriter {
86-
return l.swap([severity.NumSeverity]flushSyncWriter{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
86+
func (l *loggingT) newBuffers() [severity.NumSeverity]io.Writer {
87+
return l.swap([severity.NumSeverity]io.Writer{new(flushBuffer), new(flushBuffer), new(flushBuffer), new(flushBuffer)})
8788
}
8889

8990
// contents returns the specified log value as a string.
@@ -540,14 +541,17 @@ func TestOpenAppendOnStart(t *testing.T) {
540541

541542
// Logging creates the file
542543
Info(x)
543-
_, ok := logging.file[severity.InfoLog].(*syncBuffer)
544+
sb, ok := logging.file[severity.InfoLog].(*syncBuffer)
544545
if !ok {
545546
t.Fatal("info wasn't created")
546547
}
547548

548549
// ensure we wrote what we expected
549-
files := logging.flushAll()
550-
logging.syncAll(files)
550+
needToSync := logging.flushAll()
551+
if needToSync.num != 1 || needToSync.files[0] != sb.file {
552+
t.Errorf("Should have received exactly the file from severity.InfoLog for syncing, got instead: %+v", needToSync)
553+
}
554+
logging.syncAll(needToSync)
551555
b, err := ioutil.ReadFile(logging.logFile)
552556
if err != nil {
553557
t.Fatalf("unexpected error: %v", err)
@@ -811,15 +815,58 @@ func BenchmarkLogs(b *testing.B) {
811815
Severity: severity.FatalLog,
812816
}
813817
logging.logFile = testFile.Name()
814-
logging.swap([severity.NumSeverity]flushSyncWriter{nil, nil, nil, nil})
818+
logging.swap([severity.NumSeverity]io.Writer{nil, nil, nil, nil})
815819

816820
for i := 0; i < b.N; i++ {
817821
Error("error")
818822
Warning("warning")
819823
Info("info")
820824
}
821-
files := logging.flushAll()
822-
logging.syncAll(files)
825+
needToSync := logging.flushAll()
826+
sb, ok := logging.file[severity.InfoLog].(*syncBuffer)
827+
if !ok {
828+
b.Fatal("info wasn't created")
829+
}
830+
if needToSync.num != 1 || needToSync.files[0] != sb.file {
831+
b.Fatalf("Should have received exactly the file from severity.InfoLog for syncing, got instead: %+v", needToSync)
832+
}
833+
logging.syncAll(needToSync)
834+
}
835+
836+
func BenchmarkFlush(b *testing.B) {
837+
defer CaptureState().Restore()
838+
setFlags()
839+
defer logging.swap(logging.newBuffers())
840+
841+
testFile, err := ioutil.TempFile("", "test.log")
842+
if err != nil {
843+
b.Fatal("unable to create temporary file")
844+
}
845+
defer os.Remove(testFile.Name())
846+
847+
require.NoError(b, logging.verbosity.Set("0"))
848+
logging.toStderr = false
849+
logging.alsoToStderr = false
850+
logging.stderrThreshold = severityValue{
851+
Severity: severity.FatalLog,
852+
}
853+
logging.logFile = testFile.Name()
854+
logging.swap([severity.NumSeverity]io.Writer{nil, nil, nil, nil})
855+
856+
// Create output file.
857+
Info("info")
858+
needToSync := logging.flushAll()
859+
860+
if needToSync.num != 1 {
861+
b.Fatalf("expected exactly one file to sync, got: %+v", needToSync)
862+
}
863+
864+
b.ResetTimer()
865+
866+
for i := 0; i < b.N; i++ {
867+
needToSync := logging.flushAll()
868+
logging.syncAll(needToSync)
869+
}
823870
}
824871

825872
// Test the logic on checking log size limitation.

0 commit comments

Comments
 (0)