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  }