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