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
11 changes: 7 additions & 4 deletions connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ type Connection struct {
inboundCtx context.Context
inboundCancel context.CancelCauseFunc

logger *slog.Logger
// logger is atomic because SetLogger may be called after NewConnection has
// already spawned the receive/processNotifications goroutines that read it
// via loggerOrDefault.
logger atomic.Pointer[slog.Logger]

notifyMu sync.Mutex
// notifyCond coordinates response-scoped waits for sequential notification processing.
Expand Down Expand Up @@ -122,11 +125,11 @@ func NewConnection(handler MethodHandler, peerInput io.Writer, peerOutput io.Rea

// SetLogger installs a logger used for internal connection diagnostics.
// If unset, logs are written via the default logger.
func (c *Connection) SetLogger(l *slog.Logger) { c.logger = l }
func (c *Connection) SetLogger(l *slog.Logger) { c.logger.Store(l) }

func (c *Connection) loggerOrDefault() *slog.Logger {
if c.logger != nil {
return c.logger
if l := c.logger.Load(); l != nil {
return l
}
return slog.Default()
}
Expand Down
57 changes: 57 additions & 0 deletions connection_logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package acp

import (
"context"
"encoding/json"
"io"
"log/slog"
"sync"
"testing"
)

// TestConnectionSetLogger_ConcurrentWithReceive guards the logger field against
// a data race: NewConnection spawns receive/processNotifications before the
// caller has any chance to install a logger, so a later SetLogger writes the
// field while those goroutines read it through loggerOrDefault.
//
// Unparseable input makes receive log, which is the read side of the race.
// Run under -race; without synchronization on the field this fails.
func TestConnectionSetLogger_ConcurrentWithReceive(t *testing.T) {
inR, inW := io.Pipe()
outR, outW := io.Pipe()
t.Cleanup(func() {
_ = inW.Close()
_ = outW.Close()
_ = inR.Close()
_ = outR.Close()
})

// Drain peer output so the connection never blocks on a write.
go func() { _, _ = io.Copy(io.Discard, outR) }()

c := NewConnection(func(ctx context.Context, method string, params json.RawMessage) (any, *RequestError) {
return nil, nil
}, outW, inR)

discard := slog.New(slog.NewTextHandler(io.Discard, nil))
// Install before any input arrives so the parse errors below stay off stderr.
c.SetLogger(discard)

const iterations = 100

var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < iterations; i++ {
if _, err := inW.Write([]byte("not json\n")); err != nil {
return
}
}
}()

for i := 0; i < iterations; i++ {
c.SetLogger(discard)
}
wg.Wait()
}