Skip to content

Commit 4228a15

Browse files
committed
slog-handler-guide: explain pointer to mutex
Change-Id: Icfca54f04ff1fcc2c2b36cb0ccde1efd49e034f4 Reviewed-on: https://go-review.googlesource.com/c/example/+/512999 Reviewed-by: Ian Cottrell <[email protected]> Run-TryBot: Jonathan Amsterdam <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 94c9d89 commit 4228a15

File tree

2 files changed

+64
-10
lines changed

2 files changed

+64
-10
lines changed

slog-handler-guide/README.md

+32-5
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,7 @@ Changes to `LevelVar`s are goroutine-safe.
150150
The mutex will be used to ensure that writes to the `io.Writer` happen atomically.
151151
Unusually, `IndentHandler` holds a pointer to a `sync.Mutex` rather than holding a
152152
`sync.Mutex` directly.
153-
There is a good reason for that, which we'll explain later.
154-
155-
TODO(jba): add link to that later explanation.
153+
There is a good reason for that, which we'll explain [later](#getting-the-mutex-right).
156154

157155
Our handler will need additional state to track calls to `WithGroup` and `WithAttrs`.
158156
We will describe that state when we get to those methods.
@@ -508,6 +506,33 @@ you can use a linked list instead,
508506
which `Handle` will have to reverse or visit recursively.
509507
See [github.com/jba/slog/withsupport](https://github.com/jba/slog/withsupport) for an implementation.
510508

509+
#### Getting the mutex right
510+
511+
Let us revisit the last few lines of `Handle`:
512+
513+
h.mu.Lock()
514+
defer h.mu.Unlock()
515+
_, err := h.out.Write(buf)
516+
return err
517+
518+
This code hasn't changed, but we can now appreciate why `h.mu` is a
519+
pointer to a `sync.Mutex`. Both `WithGroup` and `WithAttrs` copy the handler.
520+
Both copies point to the same mutex.
521+
If the copy and the original used different mutexes and were used concurrently,
522+
then their output could be interleaved, or some output could be lost.
523+
Code like this:
524+
525+
l2 := l1.With("a", 1)
526+
go l1.Info("hello")
527+
l2.Info("goodbye")
528+
529+
could produce output like this:
530+
531+
hegoollo a=dbye1
532+
533+
See [this bug report](https://go.dev/issue/61321) for more detail.
534+
535+
511536
### With pre-formatting
512537

513538
Our second implementation implements pre-formatting.
@@ -708,8 +733,10 @@ mutable state.
708733
- The `Handle` method typically works only with its arguments and stored fields.
709734

710735
Calls to output methods like `io.Writer.Write` should be synchronized unless
711-
it can be verified that no locking is needed. Beware of facile claims like
712-
"Unix writes are atomic"; the situation is a lot more nuanced than that.
736+
it can be verified that no locking is needed.
737+
As we saw in our example, storing a pointer to a mutex enables a logger and
738+
all of its clones to synchronize with each other.
739+
Beware of facile claims like "Unix writes are atomic"; the situation is a lot more nuanced than that.
713740

714741
Some handlers have legitimate reasons for keeping state.
715742
For example, a handler might support a `SetLevel` method to change its configured level

slog-handler-guide/guide.md

+32-5
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,7 @@ Changes to `LevelVar`s are goroutine-safe.
113113
The mutex will be used to ensure that writes to the `io.Writer` happen atomically.
114114
Unusually, `IndentHandler` holds a pointer to a `sync.Mutex` rather than holding a
115115
`sync.Mutex` directly.
116-
There is a good reason for that, which we'll explain later.
117-
118-
TODO(jba): add link to that later explanation.
116+
There is a good reason for that, which we'll explain [later](#getting-the-mutex-right).
119117

120118
Our handler will need additional state to track calls to `WithGroup` and `WithAttrs`.
121119
We will describe that state when we get to those methods.
@@ -328,6 +326,33 @@ you can use a linked list instead,
328326
which `Handle` will have to reverse or visit recursively.
329327
See [github.com/jba/slog/withsupport](https://github.com/jba/slog/withsupport) for an implementation.
330328

329+
#### Getting the mutex right
330+
331+
Let us revisit the last few lines of `Handle`:
332+
333+
h.mu.Lock()
334+
defer h.mu.Unlock()
335+
_, err := h.out.Write(buf)
336+
return err
337+
338+
This code hasn't changed, but we can now appreciate why `h.mu` is a
339+
pointer to a `sync.Mutex`. Both `WithGroup` and `WithAttrs` copy the handler.
340+
Both copies point to the same mutex.
341+
If the copy and the original used different mutexes and were used concurrently,
342+
then their output could be interleaved, or some output could be lost.
343+
Code like this:
344+
345+
l2 := l1.With("a", 1)
346+
go l1.Info("hello")
347+
l2.Info("goodbye")
348+
349+
could produce output like this:
350+
351+
hegoollo a=dbye1
352+
353+
See [this bug report](https://go.dev/issue/61321) for more detail.
354+
355+
331356
### With pre-formatting
332357

333358
Our second implementation implements pre-formatting.
@@ -451,8 +476,10 @@ mutable state.
451476
- The `Handle` method typically works only with its arguments and stored fields.
452477

453478
Calls to output methods like `io.Writer.Write` should be synchronized unless
454-
it can be verified that no locking is needed. Beware of facile claims like
455-
"Unix writes are atomic"; the situation is a lot more nuanced than that.
479+
it can be verified that no locking is needed.
480+
As we saw in our example, storing a pointer to a mutex enables a logger and
481+
all of its clones to synchronize with each other.
482+
Beware of facile claims like "Unix writes are atomic"; the situation is a lot more nuanced than that.
456483

457484
Some handlers have legitimate reasons for keeping state.
458485
For example, a handler might support a `SetLevel` method to change its configured level

0 commit comments

Comments
 (0)