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  }