fix(diary): retire replaced daily writers and serialise ApplyGlobals

- install and Reset retire the channels they replace: a retired daily
  writer appends a late record and closes the file again, so no record is
  lost and no descriptor stays open; diary.Close keeps reopening the
  installed writers on the next record
- ApplyGlobals holds the setup lock for its whole run (lock-free
  installLocked and setVerbosityLocked), so concurrent calls rebuild the
  channels once (WR-06)
This commit is contained in:
Jakub Zych
2026-10-22 02:27:39 +02:00
parent c586c6f8a9
commit 39677f1819
3 changed files with 179 additions and 8 deletions

View File

@@ -156,6 +156,12 @@ func Setup(cfg *compass.Config, opts Options) error {
func install(conf Config, ch *channels, opts Options) {
mu.Lock()
defer mu.Unlock()
installLocked(conf, ch, opts)
}
// installLocked makes ch the installed setup and retires the channels it
// replaces; the caller holds mu.
func installLocked(conf Config, ch *channels, opts Options) {
if saved == nil {
saved = &savedDefault{logger: slog.Default(), writer: log.Writer(), flags: log.Flags()}
}
@@ -174,7 +180,7 @@ func install(conf Config, ch *channels, opts Options) {
old := current.Swap(st)
slog.SetDefault(slog.New(newComponentHandler("")))
if old != nil {
closeChannels(old.channels)
retireChannels(old.channels)
}
}
@@ -186,6 +192,11 @@ func install(conf Config, ch *channels, opts Options) {
func SetVerbosity(v int, quiet bool) {
mu.Lock()
defer mu.Unlock()
setVerbosityLocked(v, quiet)
}
// setVerbosityLocked is SetVerbosity for a caller that holds mu.
func setVerbosityLocked(v int, quiet bool) {
verbosity.Store(int64(v))
st := current.Load()
if st == nil {
@@ -212,18 +223,21 @@ func FromGlobals(g bonfire.Globals) Options {
// the verbosity and quiet switch (SetVerbosity) and, when it differs from
// the one the channels were built with, the console color mode, which
// rebuilds the channels from the same configuration. The generated main
// registers it with bonfire.OnGlobals.
// registers it with bonfire.OnGlobals. It holds the setup lock for its
// whole run, so concurrent calls rebuild the channels once.
func ApplyGlobals(g bonfire.Globals) {
mu.Lock()
defer mu.Unlock()
st := current.Load()
if st != nil && st.opts.Color != g.Color {
opts := st.opts
opts.Color, opts.Verbosity, opts.Quiet = g.Color, g.Verbosity, g.Quiet
if ch, err := buildChannels(st.conf, opts); err == nil {
install(st.conf, ch, opts)
installLocked(st.conf, ch, opts)
return
}
}
SetVerbosity(g.Verbosity, g.Quiet)
setVerbosityLocked(g.Verbosity, g.Quiet)
}
type hideOnTTYKey struct{}
@@ -264,6 +278,22 @@ func Close() error {
return closeChannels(st.channels)
}
// retireChannels closes the files of channels a new setup replaced, for
// good: a goroutine that still holds the old state appends its record and
// the file is closed again, so no descriptor stays open.
func retireChannels(ch *channels) {
if ch == nil {
return
}
for _, c := range ch.closers {
if r, ok := c.(interface{ retire() error }); ok {
_ = r.retire()
continue
}
_ = c.Close()
}
}
func closeChannels(ch *channels) error {
if ch == nil {
return nil
@@ -275,9 +305,9 @@ func closeChannels(ch *channels) error {
return errors.Join(errs...)
}
// Reset drops the installed setup, closes its files and restores the process
// default logger (and the standard log package's output) captured at the
// first Bootstrap or Setup. Tests call it in t.Cleanup.
// Reset drops the installed setup, closes its files for good and restores
// the process default logger (and the standard log package's output)
// captured at the first Bootstrap or Setup. Tests call it in t.Cleanup.
func Reset() {
mu.Lock()
defer mu.Unlock()
@@ -290,6 +320,6 @@ func Reset() {
saved = nil
}
if old != nil {
closeChannels(old.channels)
retireChannels(old.channels)
}
}