github.com/ydb-platform/ydb-go-sdk/v3@v3.89.2/internal/topic/topicreadercommon/committer.go (about) 1 package topicreadercommon 2 3 import ( 4 "context" 5 "errors" 6 "sync/atomic" 7 "time" 8 9 "github.com/jonboulle/clockwork" 10 11 "github.com/ydb-platform/ydb-go-sdk/v3/internal/background" 12 "github.com/ydb-platform/ydb-go-sdk/v3/internal/empty" 13 "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopiccommon" 14 "github.com/ydb-platform/ydb-go-sdk/v3/internal/grpcwrapper/rawtopic/rawtopicreader" 15 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xerrors" 16 "github.com/ydb-platform/ydb-go-sdk/v3/internal/xsync" 17 "github.com/ydb-platform/ydb-go-sdk/v3/trace" 18 ) 19 20 var ( 21 ErrCommitDisabled = xerrors.Wrap(errors.New("ydb: commits disabled")) 22 ErrWrongCommitOrderInSyncMode = xerrors.Wrap(errors.New("ydb: wrong commit order in sync mode")) 23 ) 24 25 type SendMessageToServerFunc func(msg rawtopicreader.ClientMessage) error 26 27 type PublicCommitMode int 28 29 const ( 30 CommitModeAsync PublicCommitMode = iota // default 31 CommitModeNone 32 CommitModeSync 33 ) 34 35 func (m PublicCommitMode) CommitsEnabled() bool { 36 return m != CommitModeNone 37 } 38 39 type Committer struct { 40 BufferTimeLagTrigger time.Duration // 0 mean no additional time lag 41 BufferCountTrigger int 42 43 send SendMessageToServerFunc 44 mode PublicCommitMode 45 46 clock clockwork.Clock 47 commitLoopSignal empty.Chan 48 backgroundWorker background.Worker 49 tracer *trace.Topic 50 51 m xsync.Mutex 52 waiters []commitWaiter 53 commits CommitRanges 54 } 55 56 func NewCommitterStopped( 57 tracer *trace.Topic, 58 lifeContext context.Context, //nolint:revive 59 mode PublicCommitMode, 60 send SendMessageToServerFunc, 61 ) *Committer { 62 res := &Committer{ 63 mode: mode, 64 clock: clockwork.NewRealClock(), 65 send: send, 66 backgroundWorker: *background.NewWorker(lifeContext, "ydb-topic-reader-committer"), 67 tracer: tracer, 68 } 69 res.initChannels() 70 71 return res 72 } 73 74 func (c *Committer) initChannels() { 75 c.commitLoopSignal = make(empty.Chan, 1) 76 } 77 78 func (c *Committer) Start() { 79 c.backgroundWorker.Start("commit pusher", c.pushCommitsLoop) 80 } 81 82 func (c *Committer) Close(ctx context.Context, err error) error { 83 return c.backgroundWorker.Close(ctx, err) 84 } 85 86 func (c *Committer) Commit(ctx context.Context, commitRange CommitRange) error { 87 if !c.mode.CommitsEnabled() { 88 return ErrCommitDisabled 89 } 90 91 if ctx.Err() != nil { 92 return ctx.Err() 93 } 94 95 waiter, err := c.pushCommit(commitRange) 96 if err != nil { 97 return err 98 } 99 100 return c.waitCommitAck(ctx, waiter) 101 } 102 103 func (c *Committer) pushCommit(commitRange CommitRange) (commitWaiter, error) { 104 var resErr error 105 waiter := newCommitWaiter(commitRange.PartitionSession, commitRange.CommitOffsetEnd) 106 c.m.WithLock(func() { 107 if err := c.backgroundWorker.Context().Err(); err != nil { 108 resErr = err 109 110 return 111 } 112 113 c.commits.Append(&commitRange) 114 if c.mode == CommitModeSync { 115 c.addWaiterNeedLock(waiter) 116 } 117 }) 118 119 select { 120 case c.commitLoopSignal <- struct{}{}: 121 default: 122 } 123 124 return waiter, resErr 125 } 126 127 func (c *Committer) pushCommitsLoop(ctx context.Context) { 128 for { 129 c.waitSendTrigger(ctx) 130 131 var commits CommitRanges 132 c.m.WithLock(func() { 133 commits = c.commits 134 c.commits = NewCommitRangesWithCapacity(commits.Len() * 2) //nolint:gomnd 135 }) 136 137 if commits.Len() == 0 && c.backgroundWorker.Context().Err() != nil { 138 // committer closed with empty buffer - target close state 139 return 140 } 141 142 // all ranges already committed of prev iteration 143 if commits.Len() == 0 { 144 continue 145 } 146 147 commits.Optimize() 148 149 onDone := trace.TopicOnReaderSendCommitMessage( 150 c.tracer, 151 &commits, 152 ) 153 err := c.send(commits.ToRawMessage()) 154 onDone(err) 155 156 if err != nil { 157 _ = c.backgroundWorker.Close(ctx, err) 158 } 159 } 160 } 161 162 func (c *Committer) waitSendTrigger(ctx context.Context) { 163 ctxDone := ctx.Done() 164 select { 165 case <-ctxDone: 166 return 167 case <-c.commitLoopSignal: 168 } 169 170 if c.BufferTimeLagTrigger == 0 { 171 return 172 } 173 174 bufferTimeLagTriggerTimer := c.clock.NewTimer(c.BufferTimeLagTrigger) 175 defer bufferTimeLagTriggerTimer.Stop() 176 177 finish := bufferTimeLagTriggerTimer.Chan() 178 if c.BufferCountTrigger == 0 { 179 select { 180 case <-ctxDone: 181 case <-finish: 182 } 183 184 return 185 } 186 187 for { 188 var commitsLen int 189 c.m.WithLock(func() { 190 commitsLen = c.commits.Len() 191 }) 192 if commitsLen >= c.BufferCountTrigger { 193 return 194 } 195 196 select { 197 case <-ctxDone: 198 return 199 case <-finish: 200 return 201 case <-c.commitLoopSignal: 202 // check count on next loop iteration 203 } 204 } 205 } 206 207 func (c *Committer) waitCommitAck(ctx context.Context, waiter commitWaiter) error { 208 if c.mode != CommitModeSync { 209 return nil 210 } 211 212 defer c.m.WithLock(func() { 213 c.removeWaiterByIDNeedLock(waiter.ID) 214 }) 215 if waiter.checkCondition(waiter.Session, waiter.Session.CommittedOffset()) { 216 return nil 217 } 218 219 select { 220 case <-ctx.Done(): 221 return ctx.Err() 222 case <-waiter.Session.Context().Done(): 223 return PublicErrCommitSessionToExpiredSession 224 case <-waiter.Committed: 225 return nil 226 } 227 } 228 229 func (c *Committer) OnCommitNotify(session *PartitionSession, offset rawtopiccommon.Offset) { 230 c.m.WithLock(func() { 231 for i := range c.waiters { 232 waiter := c.waiters[i] 233 if waiter.checkCondition(session, offset) { 234 select { 235 case waiter.Committed <- struct{}{}: 236 default: 237 } 238 } 239 } 240 }) 241 } 242 243 func (c *Committer) addWaiterNeedLock(waiter commitWaiter) { 244 c.waiters = append(c.waiters, waiter) 245 } 246 247 func (c *Committer) removeWaiterByIDNeedLock(id int64) { 248 newWaiters := c.waiters[:0] 249 for i := range c.waiters { 250 if c.waiters[i].ID == id { 251 continue 252 } 253 254 newWaiters = append(newWaiters, c.waiters[i]) 255 } 256 c.waiters = newWaiters 257 } 258 259 type commitWaiter struct { 260 ID int64 261 Session *PartitionSession 262 EndOffset rawtopiccommon.Offset 263 Committed empty.Chan 264 } 265 266 func (w *commitWaiter) checkCondition( 267 session *PartitionSession, 268 offset rawtopiccommon.Offset, 269 ) (finished bool) { 270 return session == w.Session && offset >= w.EndOffset 271 } 272 273 var commitWaiterLastID int64 274 275 func newCommitWaiter(session *PartitionSession, endOffset rawtopiccommon.Offset) commitWaiter { 276 id := atomic.AddInt64(&commitWaiterLastID, 1) 277 278 return commitWaiter{ 279 ID: id, 280 Session: session, 281 EndOffset: endOffset, 282 Committed: make(empty.Chan, 1), 283 } 284 }