Skip to content

fix(io): make logger mode concurrency-safe and fix writer mode restore - #112

Merged
jahvon merged 1 commit into
mainfrom
fix/logger-mode-datarace
Jul 26, 2026
Merged

fix(io): make logger mode concurrency-safe and fix writer mode restore#112
jahvon merged 1 commit into
mainfrom
fix/logger-mode-datarace

Conversation

@jahvon

@jahvon jahvon commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

Fixes a data race on StandardLogger.mode (reported downstream by go test -race during concurrent command execution), plus a latent bug where StdOutWriter never restored the logger's mode.

StdOutWriter/StdErrWriter temporarily flip the shared logger's mode on every Write so Info/Notice render in the desired format. When a command's stdout and stderr are copied concurrently (os/exec uses one goroutine per stream), those writers call SetMode (write) and LogMode (read) on the shared mode field with no synchronization:

Read  at ...StandardLogger.LogMode()  <- StdOutWriter.Write
Write at ...StandardLogger.SetMode()  <- StdErrWriter.Write

Changes

  • mode is now guarded by a sync.RWMutex (modeMu). SetMode write-locks; LogMode and all internal reads go through a currentMode() read-locked accessor. The underlying charm log.Logger handlers are already internally synchronized, so only this field needed protection.
  • The writers' flip/render/restore sequence is serialized via a second mutex (writeMu), exposed to the writers in-package, so two concurrent writers can't interleave and render each other's output in the wrong mode.
  • Fixed StdOutWriter's restore: it reassigned its saved curMode to the new mode after flipping, so the restore-defer condition was always false and the logger was left permanently in the flipped mode (StdErrWriter didn't have this bug). Both writers now use a single, correct flip/restore path gated on a flipped flag.

Testing

  • New regression test drives both writers concurrently with a mode different from the logger's (forcing the flip path). It reports WARNING: DATA RACE under -race without the fix and passes with it — verified both ways — and also asserts the logger's mode is restored afterward.
  • Full suite green: go test -race ./...; golangci-lint clean.

Downstream note

flow currently carries a client-side workaround (serializing these writers itself). Once this is released and flow bumps tuikit, that workaround can be removed.

🤖 Generated with Claude Code

Std{Out,Err}Writer temporarily flip the shared logger's mode on every write.
When a command's stdout and stderr are copied concurrently (os/exec uses one
goroutine per stream), those writers read (LogMode) and write (SetMode) the
StandardLogger.mode field with no synchronization — a data race under -race.

- Guard StandardLogger.mode with a RWMutex (modeMu). The underlying charm log
  handlers are already internally synchronized, so only this field needed it.
- Serialize the writers' flip/render/restore sequence with a second mutex
  (writeMu), exposed to the writers in-package, so concurrent writers cannot
  interleave and render each other's output in the wrong mode.
- Fix StdOutWriter, which reassigned its saved mode after flipping and so never
  restored the logger's original mode (StdErrWriter was already correct). Both
  writers now share one flip/restore path guarded by a `flipped` flag.

Adds a regression test that drives both writers concurrently with a mode
different from the logger's; it reports DATA RACE under -race without the fix,
and also asserts the logger's mode is restored afterward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jahvon
jahvon merged commit 4d45cee into main Jul 26, 2026
8 checks passed
@jahvon
jahvon deleted the fix/logger-mode-datarace branch July 26, 2026 15:00
@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 59.45946% with 15 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
io/logger.go 37.50% 12 Missing and 3 partials ⚠️

📢 Thoughts on this report? Let us know!

jahvon added a commit to flowexec/flow that referenced this pull request Jul 26, 2026
tuikit v0.4.1 makes StandardLogger's mode concurrency-safe and fixes the
Std{Out,Err}Writer restore path (flowexec/tuikit#112). That resolves the data
race between a command's concurrently-copied stdout and stderr streams at the
source, so no client-side workaround is needed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jahvon added a commit to flowexec/flow that referenced this pull request Jul 26, 2026
## Summary

Bumps tuikit `v0.4.0` → `v0.4.1`, which fixes the logger data race at
the source (flowexec/tuikit#112) instead of working around it in flow.

The race was between a command's stdout and stderr streams (copied by
os/exec in separate goroutines) both flipping `StandardLogger`'s shared
`mode` field. tuikit v0.4.1:
- guards the `mode` field with a mutex,
- serializes the writers' flip/render/restore sequence, and
- fixes `StdOutWriter` never restoring the mode after a flip.

Since the fix lives in the dependency, no client-side change is needed
here beyond the version bump — this PR previously carried a `syncWriter`
workaround, now removed in favor of the upstream fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant