11package ingester
22
33import (
4- "sync"
5-
64 "github.com/dustin/go-humanize"
75 "github.com/go-kit/log/level"
86 "go.uber.org/atomic"
7+ "golang.org/x/sync/singleflight"
98
109 util_log "github.com/grafana/loki/v3/pkg/util/log"
1110)
@@ -49,17 +48,16 @@ type replayController struct {
4948 currentBytes atomic.Int64
5049 cfg WALConfig
5150 metrics * ingesterMetrics
52- cond * sync. Cond
53- isFlushing atomic. Bool
54- flusher Flusher
51+
52+ flusher Flusher
53+ flushSF singleflight. Group
5554}
5655
5756// flusher is expected to reduce pressure via calling Sub
5857func newReplayController (metrics * ingesterMetrics , cfg WALConfig , flusher Flusher ) * replayController {
5958 return & replayController {
6059 cfg : cfg ,
6160 metrics : metrics ,
62- cond : sync .NewCond (& sync.Mutex {}),
6361 flusher : flusher ,
6462 }
6563}
@@ -79,51 +77,41 @@ func (c *replayController) Cur() int {
7977}
8078
8179func (c * replayController ) Flush () {
82- if c .isFlushing .CompareAndSwap (false , true ) {
83- c .metrics .recoveryIsFlushing .Set (1 )
84- prior := c .currentBytes .Load ()
85- level .Debug (util_log .Logger ).Log (
86- "msg" , "replay flusher pre-flush" ,
87- "bytes" , humanize .Bytes (uint64 (prior )),
88- )
89-
90- c .flusher .Flush ()
91-
92- after := c .currentBytes .Load ()
93- level .Debug (util_log .Logger ).Log (
94- "msg" , "replay flusher post-flush" ,
95- "bytes" , humanize .Bytes (uint64 (after )),
96- )
97-
98- c .isFlushing .Store (false )
99- c .metrics .recoveryIsFlushing .Set (0 )
100-
101- // Broadcast after lock is acquired to prevent race conditions with cpu scheduling
102- // where the flush code could finish before the goroutine which initiated it gets to call
103- // c.cond.Wait()
104- c .cond .L .Lock ()
105- c .cond .Broadcast ()
106- c .cond .L .Unlock ()
107- }
80+ // Use singleflight to ensure only one flush happens at a time
81+ _ , _ , _ = c .flushSF .Do ("flush" , func () (interface {}, error ) {
82+ c .flush ()
83+ return nil , nil
84+ })
85+ }
86+
87+ func (c * replayController ) flush () {
88+ c .metrics .recoveryIsFlushing .Set (1 )
89+ prior := c .currentBytes .Load ()
90+ level .Debug (util_log .Logger ).Log (
91+ "msg" , "replay flusher pre-flush" ,
92+ "bytes" , humanize .Bytes (uint64 (prior )),
93+ )
94+
95+ c .flusher .Flush ()
96+
97+ after := c .currentBytes .Load ()
98+ level .Debug (util_log .Logger ).Log (
99+ "msg" , "replay flusher post-flush" ,
100+ "bytes" , humanize .Bytes (uint64 (after )),
101+ )
102+
103+ c .metrics .recoveryIsFlushing .Set (0 )
108104}
109105
110106// WithBackPressure is expected to call replayController.Add in the passed function to increase the managed byte count.
111107// It will call the function as long as there is expected room before the memory cap and will then flush data intermittently
112108// when needed.
113109func (c * replayController ) WithBackPressure (fn func () error ) error {
114- // Account for backpressure and wait until there's enough memory to continue replaying the WAL
115- c .cond .L .Lock ()
116-
117110 // use 90% as a threshold since we'll be adding to it.
118111 for c .Cur () > int (c .cfg .ReplayMemoryCeiling )* 9 / 10 {
119112 // too much backpressure, flush
120- go c .Flush ()
121- c .cond .Wait ()
113+ c .Flush ()
122114 }
123115
124- // Don't hold the lock while executing the provided function.
125- // This ensures we can run functions concurrently.
126- c .cond .L .Unlock ()
127-
128116 return fn ()
129117}
0 commit comments