coder / coder/acp-go-sdk

Data race: SetLogger writes logger field unsynchronized; NewConnection already started goroutines

Open
#57 1 comment 0 reactions 0 assignees View on GitHub
Dominant language
Go
Stars
230
Forks
37
PR merge metrics
No merged PRs in 30d

Description

## Summary

**Data race in `Connection.SetLogger`**: `NewConnection` spawns the `receive()` and `processNotifications()` goroutines in its constructor, and `SetLogger` writes the `logger` field with no synchronization. The goroutines read the same field concurrently via `loggerOrDefault()`. This is a guaranteed data race any time `SetLogger` is called after construction.

## How it bites

`go test -race` aborts in any client that calls `conn.SetLogger(...)` after creating the connection (the common pattern — there is no way to set the logger at construction time). In our project this fails CI in every ACP subprocess package: agy, claudecode, copilot, cursor, gemini, and the generic client.

## Reproduce

```go
conn := NewClientSideConnection(client, stdin, stdout)
conn.SetLogger(slog.New(slog.DiscardHandler)) // races with receive goroutine
```

Run with `-race`. The read is observed in `loggerOrDefault()` from the `receive()`/`shutdownReceive()` goroutine; the write is `SetLogger`.

## Suggested fix

Make the logger field race-free. Since it is a single independently-replaced immutable pointer accessed on a hot path, an atomic is the simplest correct fix:

```go
import "sync/atomic"

logger atomic.Pointer[slog.Logger]

func (c *Connection) SetLogger(l *slog.Logger) { c.logger.Store(l) }

func (c *Connection) loggerOrDefault() *slog.Logger {
if l := c.logger.Load(); l != nil {
return l
}
return slog.Default()
}
```

A mutex/`RWMutex` also works; an atomic avoids lock overhead on the hot `loggerOrDefault()` path and any lock-reentrancy interplay with the connection's existing mutexes. Alternatively, adding a constructor option that installs the logger before the goroutines start would make the field effectively immutable, but atomic storage still keeps post-construction `SetLogger` calls safe.

I can open a PR with the atomic patch if preferred.

Contributor guide

No contributing guide indexed for this repository

Research direction

Locate Connection, NewConnection, SetLogger, and loggerOrDefault, then run go test -race against the affected ACP subprocess packages to reproduce the report. Make logger access race-free while preserving post-construction SetLogger calls, and confirm the race detector passes for agy, claudecode, copilot, cursor, gemini, and the generic client.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend-api-design
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.