Files
milvus-io--milvus/internal/flushcommon/writebuffer/write_buffer.go
T
wehub-resource-sync 498b235461
Build and test / Build and test AMD64 Ubuntu 22.04 (push) Failing after 0s
Publish Builder / amazonlinux2023 (push) Failing after 1s
Build and test / UT for Go (push) Has been skipped
Publish KRTE Images / KRTE (push) Failing after 1s
Build and test / Integration Test (push) Has been skipped
Build and test / Upload Code Coverage (push) Has been skipped
Publish Builder / rockylinux9 (push) Failing after 1s
Publish Builder / ubuntu22.04 (push) Failing after 0s
Publish Builder / ubuntu24.04 (push) Failing after 0s
Publish Gpu Builder / publish-gpu-builder (push) Failing after 1s
Publish Test Images / PyTest (push) Failing after 0s
Build and test / UT for Cpp (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:17 +08:00

1753 lines
58 KiB
Go

package writebuffer
import (
"context"
"fmt"
"path"
"strings"
"sync"
"time"
"github.com/cockroachdb/errors"
"github.com/samber/lo"
"go.uber.org/atomic"
"golang.org/x/time/rate"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/msgpb"
"github.com/milvus-io/milvus-proto/go-api/v3/schemapb"
"github.com/milvus-io/milvus/internal/allocator"
"github.com/milvus-io/milvus/internal/flushcommon/metacache"
"github.com/milvus-io/milvus/internal/flushcommon/metacache/pkoracle"
"github.com/milvus-io/milvus/internal/flushcommon/syncmgr"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/metrics"
"github.com/milvus-io/milvus/pkg/v3/mlog"
"github.com/milvus-io/milvus/pkg/v3/mq/msgstream"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/util/conc"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/metautil"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/retry"
"github.com/milvus-io/milvus/pkg/v3/util/tsoutil"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
const (
nonFlushTS uint64 = 0
)
const defaultGrowingSourceRetryInterval = 100 * time.Millisecond
const growingSourceSyncFailureWarnThreshold = 600
var errGrowingSourceUnavailable = errors.New("growing source is unavailable")
// WriteBuffer is the interface for channel write buffer.
// It provides abstraction for channel write buffer and pk bloom filter & L0 delta logic.
type WriteBuffer interface {
// HasSegment checks whether certain segment exists in this buffer.
HasSegment(segmentID int64) bool
// CreateNewGrowingSegment creates a new growing segment in the buffer.
CreateNewGrowingSegment(partitionID int64, segmentID int64, startPos *msgpb.MsgPosition, schemaVersion int32)
// BufferData is the method to buffer dml data msgs.
BufferData(insertMsgs []*InsertData, deleteMsgs []*msgstream.DeleteMsg, startPos, endPos *msgpb.MsgPosition, schemaVersion int32) error
// FlushTimestamp set flush timestamp for write buffer
SetFlushTimestamp(flushTs uint64)
// GetFlushTimestamp get current flush timestamp
GetFlushTimestamp() uint64
// SealSegments is the method to perform `Sync` operation with provided options.
SealSegments(ctx context.Context, segmentIDs []int64) error
// SealAllSegments seal all segments in the write buffer.
SealAllSegments(ctx context.Context)
// DropPartitions mark segments as Dropped of the partition
DropPartitions(partitionIDs []int64)
// GetCheckpoint returns current channel checkpoint.
// If there are any non-empty segment buffer, returns the earliest buffer start position.
// Otherwise, returns latest buffered checkpoint.
GetCheckpoint() *msgpb.MsgPosition
// MemorySize returns the size in bytes currently used by this write buffer.
MemorySize() int64
// EvictBuffer evicts buffer to sync manager which match provided sync policies.
EvictBuffer(policies ...SyncPolicy)
// UseGrowingSourceFlush returns true if the collection on this channel has growing-source fields.
UseGrowingSourceFlush() bool
// GetGrowingFlushProgress returns growing-source progress for the given
// segments after this write buffer has processed up to fenceTs. If segmentIDs
// is empty, all tracked growing-source segments are returned.
GetGrowingFlushProgress(ctx context.Context, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error)
// Close is the method to close and sink current buffer data.
Close(ctx context.Context, drop bool)
}
type GrowingFlushSegmentProgress struct {
SegmentID int64
TargetOffset int64
NeedReleaseHandoff bool
SourceMode metacache.FlushSourceMode
}
type checkpointCandidate struct {
segmentID int64
position *msgpb.MsgPosition
source string
}
type checkpointCandidates struct {
candidates *typeutil.ConcurrentMap[string, *checkpointCandidate]
}
type growingSourceProgress struct {
segmentID int64
targetOffset int64
syncingOffset int64
syncing bool
pendingFlush bool
pendingCommitted *growingSourcePendingCommittedFlush
nonRetryableFailure bool
batches []growingSourceProgressBatch
failureCount int64
lastFailure string
}
type growingSourcePendingCommittedFlush struct {
targetOffset int64
manifestPath string
bm25Stats map[int64]*storage.BM25Stats
insertBinlogs map[int64]*datapb.FieldBinlog
}
// growingFlushSourceDecision is the in-memory result of decideGrowingFlushSource.
// sourceType reuses metacache.FlushSourceMode so that the writeBuffer and
// the metacache share a single concept of which subsystem owns the segment's
// payload at flush time. sourceType is always FlushSourceWriteBuffer or
// FlushSourceGrowing here (never Unknown).
type growingFlushSourceDecision struct {
sourceType metacache.FlushSourceMode
sourceState syncmgr.GrowingSourceState
}
type growingSourceProgressBatch struct {
startPosition *msgpb.MsgPosition
endPosition *msgpb.MsgPosition
endOffset int64
rowNum int64
}
func (p *growingSourceProgress) firstUncommittedPosition() *msgpb.MsgPosition {
if len(p.batches) == 0 {
return nil
}
return p.batches[0].startPosition
}
func (p *growingSourceProgress) checkpointFor(offset int64) *msgpb.MsgPosition {
var checkpoint *msgpb.MsgPosition
for _, batch := range p.batches {
if batch.endOffset <= offset {
checkpoint = batch.endPosition
continue
}
break
}
return checkpoint
}
func (p *growingSourceProgress) ack(offset int64) {
keepIdx := 0
for keepIdx < len(p.batches) && p.batches[keepIdx].endOffset <= offset {
keepIdx++
}
p.batches = p.batches[keepIdx:]
if p.pendingCommitted != nil && offset >= p.pendingCommitted.targetOffset {
p.pendingCommitted = nil
}
p.syncing = false
p.syncingOffset = 0
p.failureCount = 0
p.lastFailure = ""
}
func (p *growingSourceProgress) failSync(err error) {
p.syncing = false
p.syncingOffset = 0
p.failureCount++
if err != nil {
p.lastFailure = err.Error()
}
}
func (p *growingSourceProgress) markNonRetryableFailure() {
p.nonRetryableFailure = true
}
func isGrowingSourceLayoutMismatch(err error) bool {
if err == nil {
return false
}
msg := err.Error()
return strings.Contains(msg, "Column count mismatch") ||
strings.Contains(msg, "Column group size mismatch")
}
func cloneBM25StatsMap(stats map[int64]*storage.BM25Stats) map[int64]*storage.BM25Stats {
if len(stats) == 0 {
return nil
}
cloned := make(map[int64]*storage.BM25Stats, len(stats))
for fieldID, stat := range stats {
if stat != nil {
cloned[fieldID] = stat.Clone()
}
}
return cloned
}
func getCandidatesKey(segmentID int64, timestamp uint64) string {
return fmt.Sprintf("%d-%d", segmentID, timestamp)
}
func newCheckpointCandiates() *checkpointCandidates {
return &checkpointCandidates{
candidates: typeutil.NewConcurrentMap[string, *checkpointCandidate](), // segmentID-ts
}
}
func (c *checkpointCandidates) Remove(segmentID int64, timestamp uint64) {
c.candidates.Remove(getCandidatesKey(segmentID, timestamp))
}
func (c *checkpointCandidates) Add(segmentID int64, position *msgpb.MsgPosition, source string) {
c.candidates.Insert(getCandidatesKey(segmentID, position.GetTimestamp()), &checkpointCandidate{segmentID, position, source})
}
func (c *checkpointCandidates) GetEarliestWithDefault(def *checkpointCandidate) *checkpointCandidate {
result := def
c.candidates.Range(func(_ string, candidate *checkpointCandidate) bool {
if result == nil || candidate.position.GetTimestamp() < result.position.GetTimestamp() {
result = candidate
}
return true
})
return result
}
func NewWriteBuffer(channel string, metacache metacache.MetaCache, syncMgr syncmgr.SyncManager, opts ...WriteBufferOption) (WriteBuffer, error) {
option := defaultWBOption(metacache)
for _, opt := range opts {
opt(option)
}
return NewL0WriteBuffer(channel, metacache, syncMgr, option)
}
// writeBufferBase is the common component for buffering data
type writeBufferBase struct {
collectionID int64
channelName string
metaWriter syncmgr.MetaWriter
allocator allocator.Interface
estSizePerRecord int
metaCache metacache.MetaCache
mut sync.RWMutex
buffers map[int64]*segmentBuffer // segmentID => segmentBuffer
syncPolicies []SyncPolicy
syncCheckpoint *checkpointCandidates
syncMgr syncmgr.SyncManager
checkpoint *msgpb.MsgPosition
processedTs uint64
flushTimestamp *atomic.Uint64
errHandler func(err error)
taskObserverCallback func(t syncmgr.Task, err error) // execute when a sync task finished, should be concurrent safe.
// growing-source collection flag. growing-source can be flushed either from an optional growing
// segment source or from WriteBuffer payload when no growing source is usable.
useGrowingSourceFlush bool
growingSourceResolver GrowingSourceResolver
// growingSourceProgress tracks per-segment progress for segments backed by
// an external growing source (FlushSourceGrowing). The sticky source
// decision itself lives in metacache.SegmentInfo.flushSourceMode
growingSourceProgress map[int64]*growingSourceProgress
growingSourceRetryInterval time.Duration
growingSourceRetryScheduled bool
growingSourceRetryTimer *time.Timer
flushSourceModeNotifier FlushSourceModeNotifier
closed bool
// pre build logger
logger *mlog.Logger
cpRatedLogger *mlog.Logger
growingSourceRatedLogger *mlog.Logger
}
func newWriteBufferBase(channel string, metacache metacache.MetaCache, syncMgr syncmgr.SyncManager, option *writeBufferOption) (*writeBufferBase, error) {
flushTs := atomic.NewUint64(nonFlushTS)
flushTsPolicy := GetFlushTsPolicy(flushTs, metacache)
option.syncPolicies = append(option.syncPolicies, flushTsPolicy)
schema := metacache.GetSchema(0)
estSize, err := typeutil.EstimateSizePerRecord(schema)
if err != nil {
return nil, err
}
useGrowingSourceFlush := typeutil.UseGrowingSourceFlush(schema,
paramtable.Get().CommonCfg.UseLoonFFI.GetAsBool(),
paramtable.Get().CommonCfg.EnableGrowingSourceFlush.GetAsBool())
growingSourceResolver := option.growingSourceResolver
if growingSourceResolver == nil {
// No custom resolver means use the process-local growing source registry.
// If registry lookup misses, growing-source data falls back to WriteBuffer.
growingSourceResolver = func(segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) (syncmgr.GrowingFlushSource, syncmgr.GrowingSourceState) {
return syncmgr.DefaultGrowingSourceRegistry().Resolve(channel, segmentID, targetOffset, endPos)
}
}
growingSourceRetryInterval := option.growingSourceRetryInterval
if growingSourceRetryInterval == 0 {
growingSourceRetryInterval = defaultGrowingSourceRetryInterval
}
wb := &writeBufferBase{
channelName: channel,
collectionID: metacache.Collection(),
estSizePerRecord: estSize,
syncMgr: syncMgr,
metaWriter: option.metaWriter,
allocator: option.idAllocator,
buffers: make(map[int64]*segmentBuffer),
metaCache: metacache,
syncCheckpoint: newCheckpointCandiates(),
syncPolicies: option.syncPolicies,
flushTimestamp: flushTs,
errHandler: option.errorHandler,
taskObserverCallback: option.taskObserverCallback,
useGrowingSourceFlush: useGrowingSourceFlush,
growingSourceResolver: growingSourceResolver,
growingSourceProgress: make(map[int64]*growingSourceProgress),
growingSourceRetryInterval: growingSourceRetryInterval,
flushSourceModeNotifier: option.flushSourceModeNotifier,
}
wb.logger = mlog.With(mlog.Int64("collectionID", wb.collectionID),
mlog.String("channel", wb.channelName))
wb.cpRatedLogger = wb.logger
wb.growingSourceRatedLogger = wb.logger
return wb, nil
}
func (wb *writeBufferBase) updateProcessedTsLocked(ts uint64) {
if ts > wb.processedTs {
wb.processedTs = ts
}
}
func (wb *writeBufferBase) HasSegment(segmentID int64) bool {
wb.mut.RLock()
defer wb.mut.RUnlock()
_, ok := wb.buffers[segmentID]
return ok
}
func (wb *writeBufferBase) SealSegments(ctx context.Context, segmentIDs []int64) error {
wb.mut.Lock()
defer wb.mut.Unlock()
return wb.sealSegments(ctx, segmentIDs)
}
func (wb *writeBufferBase) SealAllSegments(ctx context.Context) {
wb.mut.Lock()
defer wb.mut.Unlock()
// mark all segments sealed if they were growing
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Sealed),
metacache.WithSegmentState(commonpb.SegmentState_Growing))
for _, progress := range wb.growingSourceProgress {
progress.pendingFlush = true
}
}
func (wb *writeBufferBase) DropPartitions(partitionIDs []int64) {
wb.mut.RLock()
defer wb.mut.RUnlock()
wb.dropPartitions(partitionIDs)
}
func (wb *writeBufferBase) SetFlushTimestamp(flushTs uint64) {
wb.mut.Lock()
defer wb.mut.Unlock()
wb.flushTimestamp.Store(flushTs)
wb.updateProcessedTsLocked(flushTs)
}
func (wb *writeBufferBase) GetFlushTimestamp() uint64 {
return wb.flushTimestamp.Load()
}
func (wb *writeBufferBase) UseGrowingSourceFlush() bool {
return wb.useGrowingSourceFlush
}
func (wb *writeBufferBase) CheckReleaseManualFlushNeed(segmentIDs []int64) bool {
if len(segmentIDs) == 0 {
return false
}
wb.mut.RLock()
defer wb.mut.RUnlock()
for _, segmentID := range segmentIDs {
segment, ok := wb.metaCache.GetSegmentByID(segmentID)
if !ok {
return true
}
switch segment.FlushSourceMode() {
case metacache.FlushSourceWriteBuffer:
continue
case metacache.FlushSourceGrowing:
if segment.State() == commonpb.SegmentState_Flushed {
continue
}
return true
default:
return true
}
}
return false
}
func (wb *writeBufferBase) GetGrowingFlushProgress(ctx context.Context, segmentIDs []int64, fenceTs uint64) ([]GrowingFlushSegmentProgress, error) {
if err := wb.waitProcessed(ctx, fenceTs); err != nil {
return nil, err
}
wb.mut.RLock()
if len(segmentIDs) == 0 {
segmentIDs = lo.Keys(wb.growingSourceProgress)
}
progresses := make([]GrowingFlushSegmentProgress, 0, len(segmentIDs))
releaseSegments := make([]syncmgr.GrowingSourceReleaseHandoffSegment, 0, len(segmentIDs))
for _, segmentID := range segmentIDs {
progress := GrowingFlushSegmentProgress{
SegmentID: segmentID,
SourceMode: metacache.FlushSourceUnknown,
}
if segment, ok := wb.metaCache.GetSegmentByID(segmentID); ok {
progress.SourceMode = segment.FlushSourceMode()
}
if growingProgress, ok := wb.growingSourceProgress[segmentID]; ok {
progress.TargetOffset = growingProgress.targetOffset
progress.NeedReleaseHandoff = wb.growingProgressRequiresHandoff(segmentID, growingProgress)
progress.SourceMode = metacache.FlushSourceGrowing
}
if progress.NeedReleaseHandoff {
releaseSegments = append(releaseSegments, syncmgr.GrowingSourceReleaseHandoffSegment{
SegmentID: segmentID,
TargetOffset: progress.TargetOffset,
})
}
progresses = append(progresses, progress)
}
wb.mut.RUnlock()
if len(releaseSegments) > 0 {
if err := syncmgr.DefaultGrowingSourceRegistry().PrepareGrowingSourceReleaseHandoff(ctx, wb.channelName, fenceTs, releaseSegments); err != nil {
return nil, err
}
}
return progresses, nil
}
func (wb *writeBufferBase) growingProgressRequiresHandoff(segmentID int64, progress *growingSourceProgress) bool {
if progress == nil {
return false
}
if len(progress.batches) > 0 {
return true
}
segment, ok := wb.metaCache.GetSegmentByID(segmentID)
if !ok {
return false
}
return segment.FlushSourceMode() == metacache.FlushSourceGrowing &&
segment.State() != commonpb.SegmentState_Flushed
}
func (wb *writeBufferBase) waitProcessed(ctx context.Context, fenceTs uint64) error {
if fenceTs == 0 {
return nil
}
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
wb.mut.RLock()
processed := wb.processedTs
closed := wb.closed
wb.mut.RUnlock()
if processed >= fenceTs {
return nil
}
if closed {
return merr.WrapErrChannelNotFound(wb.channelName)
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
func (wb *writeBufferBase) MemorySize() int64 {
wb.mut.RLock()
defer wb.mut.RUnlock()
var size int64
for _, segBuf := range wb.buffers {
size += segBuf.MemorySize()
}
return size
}
func (wb *writeBufferBase) EvictBuffer(policies ...SyncPolicy) {
logger := wb.logger
wb.mut.Lock()
// need valid checkpoint before triggering syncing
if wb.checkpoint == nil {
wb.mut.Unlock()
logger.Warn(context.TODO(), "evict buffer before buffering data")
return
}
ts := wb.checkpoint.GetTimestamp()
segmentIDs := wb.getSegmentsToSync(ts, policies...)
var syncTasks []syncmgr.Task
if len(segmentIDs) > 0 {
logger.Info(context.TODO(), "evict buffer find segments to sync", mlog.Int64s("segmentIDs", segmentIDs))
syncTasks = wb.getSyncTasksLocked(context.Background(), segmentIDs)
}
wb.mut.Unlock()
if len(syncTasks) > 0 {
futures := wb.submitSyncTasks(context.Background(), syncTasks)
if len(futures) > 0 {
conc.AwaitAll(futures...)
}
}
}
func (wb *writeBufferBase) GetCheckpoint() *msgpb.MsgPosition {
logger := wb.cpRatedLogger
wb.mut.RLock()
defer wb.mut.RUnlock()
candidates := lo.MapToSlice(wb.buffers, func(_ int64, buf *segmentBuffer) *checkpointCandidate {
return &checkpointCandidate{buf.segmentID, buf.EarliestPosition(), "segment buffer"}
})
candidates = lo.Filter(candidates, func(candidate *checkpointCandidate, _ int) bool {
return candidate.position != nil
})
for _, progress := range wb.growingSourceProgress {
if position := progress.firstUncommittedPosition(); position != nil {
candidates = append(candidates, &checkpointCandidate{
segmentID: progress.segmentID,
position: position,
source: "growing-source progress",
})
}
}
checkpoint := wb.syncCheckpoint.GetEarliestWithDefault(lo.MinBy(candidates, func(a, b *checkpointCandidate) bool {
return a.position.GetTimestamp() < b.position.GetTimestamp()
}))
if checkpoint == nil {
// all buffer are empty
logger.RatedDebug(context.TODO(), rate.Limit(60), "checkpoint from latest consumed msg", mlog.Uint64("cpTimestamp", wb.checkpoint.GetTimestamp()))
return wb.checkpoint
}
logger.RatedDebug(context.TODO(), rate.Limit(20), "checkpoint evaluated",
mlog.String("cpSource", checkpoint.source),
mlog.FieldSegmentID(checkpoint.segmentID),
mlog.Uint64("cpTimestamp", checkpoint.position.GetTimestamp()))
return checkpoint.position
}
func (wb *writeBufferBase) hasWriteBufferInsertPayload(segmentID int64) bool {
buffer, ok := wb.buffers[segmentID]
return ok && buffer.insertBuffer != nil && !buffer.insertBuffer.IsEmpty()
}
func (wb *writeBufferBase) hasGrowingSourceProgress(segmentID int64) bool {
_, ok := wb.growingSourceProgress[segmentID]
return ok
}
func (wb *writeBufferBase) decideGrowingFlushSource(segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) growingFlushSourceDecision {
// 1. Honor the sticky decision recorded in metacache. Once the first
// insert for a segment commits a source choice, every subsequent call
// must return the same kind so that progress / payload tracking stays
// consistent for the segment's lifetime.
if seg, ok := wb.metaCache.GetSegmentByID(segmentID); ok {
switch seg.FlushSourceMode() {
case metacache.FlushSourceGrowing:
state := wb.getGrowingSourceState(segmentID, targetOffset, endPos)
return growingFlushSourceDecision{
sourceType: metacache.FlushSourceGrowing,
sourceState: state,
}
case metacache.FlushSourceWriteBuffer:
return growingFlushSourceDecision{sourceType: metacache.FlushSourceWriteBuffer}
}
}
// 2. Fallback for the brief window where in-memory bookkeeping has been
// populated but the metacache sticky bit hasn't been set yet (e.g. on
// re-entry after a partial state).
if wb.hasGrowingSourceProgress(segmentID) {
state := wb.getGrowingSourceState(segmentID, targetOffset, endPos)
return growingFlushSourceDecision{
sourceType: metacache.FlushSourceGrowing,
sourceState: state,
}
}
if wb.hasWriteBufferInsertPayload(segmentID) {
return growingFlushSourceDecision{sourceType: metacache.FlushSourceWriteBuffer}
}
state := wb.getGrowingSourceState(segmentID, targetOffset, endPos)
if state == syncmgr.GrowingSourceUsable || state == syncmgr.GrowingSourcePending {
return growingFlushSourceDecision{
sourceType: metacache.FlushSourceGrowing,
sourceState: state,
}
}
wb.warnGrowingSourceFallback(segmentID, targetOffset, endPos)
return growingFlushSourceDecision{sourceType: metacache.FlushSourceWriteBuffer}
}
func (wb *writeBufferBase) getGrowingSource(segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) (syncmgr.GrowingFlushSource, syncmgr.GrowingSourceState) {
if wb.growingSourceResolver == nil {
return nil, syncmgr.GrowingSourceUnavailable
}
return wb.growingSourceResolver(segmentID, targetOffset, endPos)
}
func (wb *writeBufferBase) getGrowingSourceState(segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) syncmgr.GrowingSourceState {
source, state := wb.getGrowingSource(segmentID, targetOffset, endPos)
if source != nil {
source.Release()
}
return state
}
func (wb *writeBufferBase) warnGrowingSourceFallback(segmentID int64, targetOffset int64, endPos *msgpb.MsgPosition) {
if !wb.useGrowingSourceFlush {
return
}
wb.growingSourceRatedLogger.RatedWarn(context.TODO(), rate.Limit(1), "growing-source source is unavailable, fallback to WriteBuffer",
mlog.Int64("segmentID", segmentID),
mlog.Int64("targetOffset", targetOffset),
mlog.Any("endPosition", endPos),
)
}
func (wb *writeBufferBase) growingSourceProgressSyncable(segmentID int64, progress *growingSourceProgress, rollbackFlushing bool, markSealedFlushing bool) (bool, bool) {
if progress.nonRetryableFailure {
return false, false
}
if progress.syncing {
if segment, ok := wb.metaCache.GetSegmentByID(segmentID); ok &&
(segment.State() == commonpb.SegmentState_Sealed || segment.State() == commonpb.SegmentState_Flushing) {
progress.pendingFlush = true
}
return false, false
}
if progress.pendingCommitted != nil {
if markSealedFlushing {
if segment, ok := wb.metaCache.GetSegmentByID(segmentID); ok && segment.State() == commonpb.SegmentState_Sealed {
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Flushing), metacache.WithSegmentIDs(segmentID))
}
}
return true, false
}
if len(progress.batches) == 0 && !progress.pendingFlush {
return false, false
}
if len(progress.batches) == 0 {
segment, ok := wb.metaCache.GetSegmentByID(segmentID)
if !ok || (segment.State() != commonpb.SegmentState_Sealed && segment.State() != commonpb.SegmentState_Flushing) {
return false, false
}
}
checkpoint := wb.checkpoint
if len(progress.batches) > 0 {
checkpoint = progress.batches[len(progress.batches)-1].endPosition
}
if checkpoint == nil {
return false, false
}
state := wb.getGrowingSourceState(segmentID, progress.targetOffset, checkpoint)
if state == syncmgr.GrowingSourceUsable {
if markSealedFlushing {
if segment, ok := wb.metaCache.GetSegmentByID(segmentID); ok && segment.State() == commonpb.SegmentState_Sealed {
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Flushing), metacache.WithSegmentIDs(segmentID))
}
}
return true, false
}
// GetSealedSegmentsPolicy moves Sealed -> Flushing before returning the
// candidate. If the growing source is only pending, roll it back so the
// sealed segment can be selected again when the source catches up.
if rollbackFlushing {
if segment, ok := wb.metaCache.GetSegmentByID(segmentID); ok && segment.State() == commonpb.SegmentState_Flushing {
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Sealed), metacache.WithSegmentIDs(segmentID))
}
}
return false, true
}
func (wb *writeBufferBase) scheduleGrowingSourceRetryLocked() {
if wb.closed || wb.growingSourceRetryScheduled || wb.growingSourceRetryInterval < 0 || len(wb.growingSourceProgress) == 0 {
return
}
wb.growingSourceRetryScheduled = true
interval := wb.growingSourceRetryInterval
wb.growingSourceRetryTimer = time.AfterFunc(interval, wb.retryGrowingSourceProgress)
}
func (wb *writeBufferBase) retryGrowingSourceProgress() {
wb.mut.Lock()
wb.growingSourceRetryScheduled = false
wb.growingSourceRetryTimer = nil
if wb.closed || wb.checkpoint == nil || len(wb.growingSourceProgress) == 0 {
wb.mut.Unlock()
return
}
segmentIDs, retryNeeded := wb.getGrowingSourceSegmentsToRetry()
if retryNeeded {
wb.scheduleGrowingSourceRetryLocked()
}
var syncTasks []syncmgr.Task
if len(segmentIDs) > 0 {
wb.logger.Info(context.TODO(), "retry growing-source source sync", mlog.Int64s("segmentIDs", segmentIDs))
syncTasks = wb.getSyncTasksLocked(context.Background(), segmentIDs)
}
wb.mut.Unlock()
if len(syncTasks) > 0 {
futures := wb.submitSyncTasks(context.Background(), syncTasks)
if len(futures) > 0 {
conc.AwaitAll(futures...)
}
}
}
// getGrowingSourceSegmentsToRetry returns syncable growing-source progress segments. If a
// sealed segment becomes usable during retry, it is moved to Flushing before
// sync so GrowingSourceSyncTask commits it as a flushed segment.
// **NOTE** shall be invoked within mutex protection
func (wb *writeBufferBase) getGrowingSourceSegmentsToRetry() ([]int64, bool) {
segments := make([]int64, 0, len(wb.growingSourceProgress))
retryNeeded := false
for segmentID, progress := range wb.growingSourceProgress {
syncable, retry := wb.growingSourceProgressSyncable(segmentID, progress, false, true)
retryNeeded = retryNeeded || retry
if syncable {
segments = append(segments, segmentID)
}
}
return segments, retryNeeded
}
func (wb *writeBufferBase) recordGrowingSourceProgress(inData *InsertData, startPos, endPos *msgpb.MsgPosition, schemaVersion int32, targetOffset int64) {
wb.CreateNewGrowingSegment(inData.partitionID, inData.segmentID, startPos, schemaVersion)
progress, ok := wb.growingSourceProgress[inData.segmentID]
if !ok {
progress = &growingSourceProgress{
segmentID: inData.segmentID,
targetOffset: targetOffset - inData.rowNum,
}
wb.growingSourceProgress[inData.segmentID] = progress
}
progress.targetOffset += inData.rowNum
progress.batches = append(progress.batches, growingSourceProgressBatch{
startPosition: startPos,
endPosition: endPos,
endOffset: progress.targetOffset,
rowNum: inData.rowNum,
})
// SetFlushSourceMode is sticky: only the first call commits the choice,
// so we can include it unconditionally here without overriding a prior
// FlushSourceWriteBuffer decision.
wb.metaCache.UpdateSegments(metacache.SegmentActions(
metacache.SetStartPositionIfNil(startPos),
metacache.SetFlushSourceMode(metacache.FlushSourceGrowing),
wb.updateGrowingSourceBufferedRows(progress),
), metacache.WithSegmentIDs(inData.segmentID))
wb.notifyFlushSourceMode(inData.segmentID)
}
func (wb *writeBufferBase) growingSourceTargetOffset(segmentID int64, rows int64) int64 {
return wb.growingSourceBaseOffset(segmentID) + rows
}
func (wb *writeBufferBase) growingSourceBaseOffset(segmentID int64) int64 {
if progress, ok := wb.growingSourceProgress[segmentID]; ok {
return progress.targetOffset
}
if segment, ok := wb.metaCache.GetSegmentByID(segmentID); ok {
return segment.NumOfRows()
}
return 0
}
func (wb *writeBufferBase) updateGrowingSourceBufferedRows(progress *growingSourceProgress) metacache.SegmentAction {
return func(info *metacache.SegmentInfo) {
bufferedRows := progress.targetOffset - info.FlushedRows() - info.SyncingRows()
if bufferedRows < 0 {
bufferedRows = 0
}
metacache.UpdateBufferedRows(bufferedRows)(info)
}
}
func (wb *writeBufferBase) triggerSync() (segmentIDs []int64) {
segmentsToSync := wb.getSegmentsToSync(wb.checkpoint.GetTimestamp(), wb.syncPolicies...)
if len(segmentsToSync) > 0 {
mlog.Info(context.TODO(), "write buffer get segments to sync", mlog.Int64s("segmentIDs", segmentsToSync))
}
return segmentsToSync
}
func (wb *writeBufferBase) sealSegments(ctx context.Context, segmentIDs []int64) error {
existingIDs := make([]int64, 0, len(segmentIDs))
for _, segmentID := range segmentIDs {
_, ok := wb.metaCache.GetSegmentByID(segmentID)
if !ok {
if !wb.useGrowingSourceFlush {
mlog.Warn(ctx, "cannot find segment when sealSegments",
mlog.Int64("segmentID", segmentID),
mlog.String("channel", wb.channelName))
return merr.WrapErrSegmentNotFound(segmentID)
}
mlog.Info(ctx, "segment not found in WriteBuffer metaCache, skipping seal",
mlog.FieldSegmentID(segmentID),
mlog.String("channel", wb.channelName))
continue
}
existingIDs = append(existingIDs, segmentID)
if progress, ok := wb.growingSourceProgress[segmentID]; ok {
progress.pendingFlush = true
}
}
// mark segment flushing if segment was growing
if len(existingIDs) > 0 {
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Sealed),
metacache.WithSegmentIDs(existingIDs...),
metacache.WithSegmentState(commonpb.SegmentState_Growing))
}
return nil
}
func (wb *writeBufferBase) sealAllSegments(ctx context.Context) error {
allSegmentIds := wb.metaCache.GetSegmentIDsBy()
mlog.Info(ctx, "seal all segments", mlog.Int64s("segmentIDs", allSegmentIds))
// mark segment flushing if segment was growing
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Sealed),
metacache.WithSegmentIDs(allSegmentIds...),
metacache.WithSegmentState(commonpb.SegmentState_Growing))
return nil
}
func (wb *writeBufferBase) dropPartitions(partitionIDs []int64) {
// mark segment dropped if partition was dropped
segIDs := wb.metaCache.GetSegmentIDsBy(metacache.WithPartitionIDs(partitionIDs))
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Dropped),
metacache.WithSegmentIDs(segIDs...),
)
}
func (wb *writeBufferBase) syncSegments(ctx context.Context, segmentIDs []int64) []*conc.Future[struct{}] {
wb.mut.Lock()
syncTasks := wb.getSyncTasksLocked(ctx, segmentIDs)
wb.mut.Unlock()
return wb.submitSyncTasks(ctx, syncTasks)
}
// getSyncTasksLocked builds sync tasks and moves payload out of the write buffer.
// The caller must hold wb.mut and submit the returned tasks after releasing it.
func (wb *writeBufferBase) getSyncTasksLocked(ctx context.Context, segmentIDs []int64) []syncmgr.Task {
result := make([]syncmgr.Task, 0, len(segmentIDs))
for _, segmentID := range segmentIDs {
syncTask, err := wb.getSyncTask(ctx, segmentID)
if err != nil {
if errors.Is(err, merr.ErrSegmentNotFound) {
mlog.Warn(ctx, "segment not found in meta", mlog.FieldSegmentID(segmentID))
continue
} else if errors.Is(err, errGrowingSourceUnavailable) && wb.hasGrowingSourceProgress(segmentID) {
wb.rollbackGrowingSourceSyncCandidate(segmentID)
mlog.Warn(ctx, "growing source unavailable when building sync task, retry later",
mlog.Int64("segmentID", segmentID),
mlog.String("channel", wb.channelName),
mlog.Err(err))
continue
} else {
mlog.Fatal(ctx, "failed to get sync task", mlog.FieldSegmentID(segmentID), mlog.Err(err))
}
}
result = append(result, syncTask)
}
return result
}
func (wb *writeBufferBase) submitSyncTasks(ctx context.Context, syncTasks []syncmgr.Task) []*conc.Future[struct{}] {
result := make([]*conc.Future[struct{}], 0, len(syncTasks))
for _, syncTask := range syncTasks {
future, err := wb.syncMgr.SyncData(ctx, syncTask, func(err error) error {
if wb.taskObserverCallback != nil {
wb.taskObserverCallback(syncTask, err)
}
var resyncGrowingSourceSegmentID int64
if growingSourceTask, ok := syncTask.(*syncmgr.GrowingSourceSyncTask); ok {
wb.mut.Lock()
if progress, exists := wb.growingSourceProgress[growingSourceTask.SegmentID()]; exists {
if err != nil {
if growingSourceTask.HasCommittedFlush() && growingSourceTask.CommittedManifestPath() != "" {
progress.pendingCommitted = &growingSourcePendingCommittedFlush{
targetOffset: growingSourceTask.TargetOffset(),
manifestPath: growingSourceTask.CommittedManifestPath(),
bm25Stats: cloneBM25StatsMap(growingSourceTask.CommittedBM25Stats()),
insertBinlogs: growingSourceTask.CommittedInsertBinlogs(),
}
}
progress.failSync(err)
wb.rollbackGrowingSourceSyncTaskLocked(growingSourceTask)
wb.observeGrowingSourceSyncFailureLocked(growingSourceTask.SegmentID(), progress)
if isGrowingSourceLayoutMismatch(err) {
progress.markNonRetryableFailure()
mlog.Error(ctx, "growing-source source sync failed with non-retryable layout mismatch",
mlog.Int64("segmentID", growingSourceTask.SegmentID()),
mlog.Int64("targetOffset", progress.targetOffset),
mlog.String("lastFailure", progress.lastFailure))
} else {
wb.scheduleGrowingSourceRetryLocked()
}
} else {
if growingSourceTask.IsFlush() {
progress.pendingFlush = false
}
progress.ack(growingSourceTask.TargetOffset())
wb.resetGrowingSourceSyncFailureMetric(growingSourceTask.SegmentID())
if progress.pendingFlush && len(progress.batches) == 0 {
segment, ok := wb.metaCache.GetSegmentByID(growingSourceTask.SegmentID())
if !ok {
delete(wb.growingSourceProgress, growingSourceTask.SegmentID())
} else {
if segment.State() == commonpb.SegmentState_Sealed {
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Flushing), metacache.WithSegmentIDs(growingSourceTask.SegmentID()))
}
resyncGrowingSourceSegmentID = growingSourceTask.SegmentID()
}
} else if len(progress.batches) == 0 {
segment, ok := wb.metaCache.GetSegmentByID(growingSourceTask.SegmentID())
if growingSourceTask.IsFlush() || !ok ||
segment.State() == commonpb.SegmentState_Flushed ||
segment.State() == commonpb.SegmentState_Dropped {
delete(wb.growingSourceProgress, growingSourceTask.SegmentID())
}
}
}
}
wb.mut.Unlock()
}
if resyncGrowingSourceSegmentID != 0 {
wb.syncSegments(context.Background(), []int64{resyncGrowingSourceSegmentID})
}
if err != nil {
return err
}
if syncTask.StartPosition() != nil {
wb.syncCheckpoint.Remove(syncTask.SegmentID(), syncTask.StartPosition().GetTimestamp())
}
if syncTask.IsFlush() {
wb.metaCache.RemoveSegments(metacache.WithSegmentIDs(syncTask.SegmentID()))
mlog.Info(ctx, "flushed segment removed", mlog.FieldSegmentID(syncTask.SegmentID()), mlog.String("channel", syncTask.ChannelName()))
}
return nil
})
if err != nil {
if growingSourceTask, ok := syncTask.(*syncmgr.GrowingSourceSyncTask); ok {
growingSourceTask.ReleaseSource()
}
mlog.Fatal(ctx, "failed to sync data", mlog.Int64("segmentID", syncTask.SegmentID()), mlog.Err(err))
}
result = append(result, future)
}
return result
}
// getSegmentsToSync applies all policies to get segments list to sync.
// **NOTE** shall be invoked within mutex protection
func (wb *writeBufferBase) getSegmentsToSync(ts typeutil.Timestamp, policies ...SyncPolicy) []int64 {
buffers := lo.Values(wb.buffers)
segments := typeutil.NewSet[int64]()
for _, policy := range policies {
result := policy.SelectSegments(buffers, ts)
if len(result) > 0 {
mlog.Info(context.TODO(), "SyncPolicy selects segments", mlog.Int64s("segmentIDs", result), mlog.String("reason", policy.Reason()))
segments.Insert(result...)
}
}
for segmentID, progress := range wb.growingSourceProgress {
if len(policies) == 0 || wb.growingSourceProgressSelectedByPolicy(ts, segmentID, progress) {
segments.Insert(segmentID)
}
}
return lo.Filter(segments.Collect(), func(segmentID int64, _ int) bool {
progress, ok := wb.growingSourceProgress[segmentID]
if !ok {
return true
}
syncable, retry := wb.growingSourceProgressSyncable(segmentID, progress, segments.Contain(segmentID), false)
if retry {
wb.scheduleGrowingSourceRetryLocked()
}
return syncable
})
}
func (wb *writeBufferBase) growingSourceProgressSelectedByPolicy(ts typeutil.Timestamp, segmentID int64, progress *growingSourceProgress) bool {
if progress == nil {
return false
}
if progress.nonRetryableFailure {
return false
}
if progress.pendingFlush {
return true
}
segment, ok := wb.metaCache.GetSegmentByID(segmentID)
if ok {
switch segment.State() {
case commonpb.SegmentState_Sealed, commonpb.SegmentState_Flushing, commonpb.SegmentState_Dropped:
return true
}
if wb.growingSourceProgressFull(segment, progress) {
return true
}
}
startPos := progress.firstUncommittedPosition()
if startPos == nil {
return false
}
staleDuration := paramtable.Get().DataNodeCfg.SyncPeriod.GetAsDuration(time.Second)
current := tsoutil.PhysicalTime(ts)
start := tsoutil.PhysicalTime(startPos.GetTimestamp())
return current.Sub(start) > staleDuration
}
func (wb *writeBufferBase) growingSourceProgressFull(segment *metacache.SegmentInfo, progress *growingSourceProgress) bool {
if segment == nil || progress == nil {
return false
}
rows := progress.targetOffset - segment.FlushedRows() - segment.SyncingRows()
if rows <= 0 {
return false
}
if wb.estSizePerRecord <= 0 {
return false
}
thresholdRows := int64(wb.getEstBatchSize())
if thresholdRows <= 0 {
return true
}
return rows >= thresholdRows
}
func (wb *writeBufferBase) rollbackGrowingSourceSyncCandidate(segmentID int64) {
if progress, ok := wb.growingSourceProgress[segmentID]; ok {
progress.failSync(errGrowingSourceUnavailable)
wb.observeGrowingSourceSyncFailureLocked(segmentID, progress)
wb.scheduleGrowingSourceRetryLocked()
}
if segment, ok := wb.metaCache.GetSegmentByID(segmentID); ok && segment.State() == commonpb.SegmentState_Flushing {
wb.metaCache.UpdateSegments(metacache.UpdateState(commonpb.SegmentState_Sealed), metacache.WithSegmentIDs(segmentID))
}
}
func (wb *writeBufferBase) rollbackGrowingSourceSyncTaskLocked(task *syncmgr.GrowingSourceSyncTask) {
if task.BatchRows() > 0 {
wb.metaCache.UpdateSegments(metacache.AbortSyncing(task.BatchRows()), metacache.WithSegmentIDs(task.SegmentID()))
}
if task.StartPosition() != nil {
wb.syncCheckpoint.Remove(task.SegmentID(), task.StartPosition().GetTimestamp())
}
}
func (wb *writeBufferBase) observeGrowingSourceSyncFailureLocked(segmentID int64, progress *growingSourceProgress) {
metrics.DataNodeGrowingSourceSyncFailureCount.WithLabelValues(
paramtable.GetStringNodeID(),
fmt.Sprint(wb.collectionID),
wb.channelName,
).Set(float64(progress.failureCount))
if progress.failureCount < growingSourceSyncFailureWarnThreshold ||
progress.failureCount%growingSourceSyncFailureWarnThreshold != 0 {
return
}
wb.growingSourceRatedLogger.RatedWarn(context.TODO(), rate.Limit(1), "growing-source source sync keeps failing",
mlog.Int64("segmentID", segmentID),
mlog.Int64("failureCount", progress.failureCount),
mlog.Int64("targetOffset", progress.targetOffset),
mlog.String("lastFailure", progress.lastFailure),
)
}
func (wb *writeBufferBase) resetGrowingSourceSyncFailureMetric(segmentID int64) {
metrics.DataNodeGrowingSourceSyncFailureCount.WithLabelValues(
paramtable.GetStringNodeID(),
fmt.Sprint(wb.collectionID),
wb.channelName,
).Set(0)
if progress, ok := wb.growingSourceProgress[segmentID]; ok {
progress.failureCount = 0
progress.lastFailure = ""
}
}
func (wb *writeBufferBase) getOrCreateBuffer(segmentID int64, timetick uint64) *segmentBuffer {
buffer, ok := wb.buffers[segmentID]
if !ok {
var err error
buffer, err = newSegmentBuffer(segmentID, wb.metaCache.GetSchema(timetick))
if err != nil {
// TODO avoid panic here
panic(err)
}
wb.buffers[segmentID] = buffer
if wb.useGrowingSourceFlush {
wb.metaCache.UpdateSegments(
metacache.SetFlushSourceMode(metacache.FlushSourceWriteBuffer),
metacache.WithSegmentIDs(segmentID),
)
wb.notifyFlushSourceMode(segmentID)
}
}
return buffer
}
func (wb *writeBufferBase) notifyFlushSourceMode(segmentID int64) {
if wb.flushSourceModeNotifier == nil {
return
}
segment, ok := wb.metaCache.GetSegmentByID(segmentID)
if !ok {
return
}
switch mode := segment.FlushSourceMode(); mode {
case metacache.FlushSourceWriteBuffer, metacache.FlushSourceGrowing:
wb.flushSourceModeNotifier(segmentID, mode)
}
}
func (wb *writeBufferBase) yieldBuffer(segmentID int64) ([]*storage.InsertData, map[int64]*storage.BM25Stats, *storage.DeleteData, *schemapb.CollectionSchema, *TimeRange, *msgpb.MsgPosition) {
buffer, ok := wb.buffers[segmentID]
if !ok {
return nil, nil, nil, nil, nil, nil
}
// remove buffer and move it to sync manager
delete(wb.buffers, segmentID)
start := buffer.EarliestPosition()
timeRange := buffer.GetTimeRange()
insert, bm25, delta, schema := buffer.Yield()
return insert, bm25, delta, schema, timeRange, start
}
type InsertData struct {
segmentID int64
partitionID int64
data []*storage.InsertData
bm25Stats map[int64]*storage.BM25Stats
pkField []storage.FieldData
pkType schemapb.DataType
tsField []*storage.Int64FieldData
rowNum int64
intPKTs map[int64]int64
strPKTs map[string]int64
}
func NewInsertData(segmentID, partitionID int64, cap int, pkType schemapb.DataType) *InsertData {
data := &InsertData{
segmentID: segmentID,
partitionID: partitionID,
data: make([]*storage.InsertData, 0, cap),
pkField: make([]storage.FieldData, 0, cap),
pkType: pkType,
}
switch pkType {
case schemapb.DataType_Int64:
data.intPKTs = make(map[int64]int64)
case schemapb.DataType_VarChar:
data.strPKTs = make(map[string]int64)
}
return data
}
func (id *InsertData) Append(data *storage.InsertData, pkFieldData storage.FieldData, tsFieldData *storage.Int64FieldData) {
id.data = append(id.data, data)
id.pkField = append(id.pkField, pkFieldData)
id.tsField = append(id.tsField, tsFieldData)
id.rowNum += int64(data.GetRowNum())
timestamps := tsFieldData.GetDataRows().([]int64)
switch id.pkType {
case schemapb.DataType_Int64:
pks := pkFieldData.GetDataRows().([]int64)
for idx, pk := range pks {
ts, ok := id.intPKTs[pk]
if !ok || timestamps[idx] < ts {
id.intPKTs[pk] = timestamps[idx]
}
}
case schemapb.DataType_VarChar:
pks := pkFieldData.GetDataRows().([]string)
for idx, pk := range pks {
ts, ok := id.strPKTs[pk]
if !ok || timestamps[idx] < ts {
id.strPKTs[pk] = timestamps[idx]
}
}
}
}
func (id *InsertData) GetSegmentID() int64 {
return id.segmentID
}
func (id *InsertData) SetBM25Stats(bm25Stats map[int64]*storage.BM25Stats) {
id.bm25Stats = bm25Stats
}
func (id *InsertData) GetDatas() []*storage.InsertData {
return id.data
}
func (id *InsertData) pkExists(pk storage.PrimaryKey, ts uint64) bool {
var ok bool
var minTs int64
switch pk.Type() {
case schemapb.DataType_Int64:
minTs, ok = id.intPKTs[pk.GetValue().(int64)]
case schemapb.DataType_VarChar:
minTs, ok = id.strPKTs[pk.GetValue().(string)]
}
return ok && ts > uint64(minTs)
}
func (id *InsertData) batchPkExists(pks []storage.PrimaryKey, tss []uint64, hits []bool) []bool {
if len(pks) == 0 {
return nil
}
pkType := pks[0].Type()
switch pkType {
case schemapb.DataType_Int64:
for i := range pks {
if !hits[i] {
minTs, ok := id.intPKTs[pks[i].GetValue().(int64)]
hits[i] = ok && tss[i] > uint64(minTs)
}
}
case schemapb.DataType_VarChar:
for i := range pks {
if !hits[i] {
minTs, ok := id.strPKTs[pks[i].GetValue().(string)]
hits[i] = ok && tss[i] > uint64(minTs)
}
}
}
return hits
}
func (wb *writeBufferBase) CreateNewGrowingSegment(partitionID int64, segmentID int64, startPos *msgpb.MsgPosition, schemaVersion int32) {
_, ok := wb.metaCache.GetSegmentByID(segmentID)
// new segment
if !ok {
storageVersion := storage.StorageV2
manifestPath := ""
if paramtable.Get().CommonCfg.UseLoonFFI.GetAsBool() {
storageVersion = storage.StorageV3
// set manifest path when creating segment
k := metautil.JoinIDPath(wb.collectionID, partitionID, segmentID)
basePath := path.Join(paramtable.Get().MinioCfg.RootPath.GetValue(), common.SegmentInsertLogPath, k)
// ManifestEarliest for first write
manifestPath = packed.MarshalManifestPath(basePath, packed.ManifestEarliest)
}
segmentInfo := &datapb.SegmentInfo{
ID: segmentID,
PartitionID: partitionID,
CollectionID: wb.collectionID,
InsertChannel: wb.channelName,
StartPosition: startPos,
State: commonpb.SegmentState_Growing,
StorageVersion: storageVersion,
ManifestPath: manifestPath,
SchemaVersion: schemaVersion,
}
wb.metaCache.AddSegment(segmentInfo, func(_ *datapb.SegmentInfo) pkoracle.PkStat {
return pkoracle.NewBloomFilterSetWithBatchSize(wb.getEstBatchSize())
}, metacache.NewBM25StatsFactory, metacache.SetStartPosRecorded(false))
mlog.Info(context.TODO(), "add growing segment", mlog.FieldSegmentID(segmentID), mlog.String("channel", wb.channelName), mlog.Int64("storage version", storageVersion))
}
}
// bufferDelete buffers DeleteMsg into DeleteData.
func (wb *writeBufferBase) bufferDelete(segmentID int64, pks []storage.PrimaryKey, tss []typeutil.Timestamp, startPos, endPos *msgpb.MsgPosition) {
segBuf := wb.getOrCreateBuffer(segmentID, tss[0])
bufSize := segBuf.deltaBuffer.Buffer(pks, tss, startPos, endPos)
metrics.DataNodeFlowGraphBufferDataSize.WithLabelValues(paramtable.GetStringNodeID(), fmt.Sprint(wb.collectionID)).Add(float64(bufSize))
}
func (wb *writeBufferBase) getSyncTask(ctx context.Context, segmentID int64) (syncmgr.Task, error) {
segmentInfo, ok := wb.metaCache.GetSegmentByID(segmentID) // wb.metaCache.GetSegmentsBy(metacache.WithSegmentIDs(segmentID))
if !ok {
mlog.Warn(ctx, "segment info not found in meta cache", mlog.FieldSegmentID(segmentID))
return nil, merr.WrapErrSegmentNotFound(segmentID)
}
if progress, ok := wb.growingSourceProgress[segmentID]; ok && !wb.hasWriteBufferInsertPayload(segmentID) {
return wb.getGrowingSourceSyncTask(ctx, segmentInfo, progress)
}
var batchSize int64
var totalMemSize float64 = 0
var tsFrom, tsTo uint64
insert, bm25, delta, schema, timeRange, startPos := wb.yieldBuffer(segmentID)
if timeRange != nil {
tsFrom, tsTo = timeRange.timestampMin, timeRange.timestampMax
}
if startPos != nil {
wb.syncCheckpoint.Add(segmentID, startPos, "syncing task")
}
actions := []metacache.SegmentAction{}
for _, chunk := range insert {
batchSize += int64(chunk.GetRowNum())
totalMemSize += float64(chunk.GetMemorySize())
}
if delta != nil {
totalMemSize += float64(delta.Size())
}
actions = append(actions, metacache.StartSyncing(batchSize))
wb.metaCache.UpdateSegments(metacache.MergeSegmentAction(actions...), metacache.WithSegmentIDs(segmentID))
pack := &syncmgr.SyncPack{}
pack.WithInsertData(insert).
WithDeleteData(delta).
WithCollectionID(wb.collectionID).
WithPartitionID(segmentInfo.PartitionID()).
WithChannelName(wb.channelName).
WithSegmentID(segmentID).
WithStartPosition(startPos).
WithTimeRange(tsFrom, tsTo).
WithLevel(segmentInfo.Level()).
WithDataSource(metrics.StreamingDataSourceLabel).
WithCheckpoint(wb.checkpoint).
WithBatchRows(batchSize).
WithErrorHandler(wb.errHandler)
if len(bm25) != 0 {
pack.WithBM25Stats(bm25)
}
if segmentInfo.State() == commonpb.SegmentState_Flushing ||
segmentInfo.Level() == datapb.SegmentLevel_L0 { // Level zero segment will always be sync as flushed
pack.WithFlush()
}
if segmentInfo.State() == commonpb.SegmentState_Dropped {
pack.WithDrop()
}
metrics.DataNodeFlowGraphBufferDataSize.WithLabelValues(paramtable.GetStringNodeID(), fmt.Sprint(wb.collectionID)).Sub(totalMemSize)
task := syncmgr.NewSyncTask().
WithAllocator(wb.allocator).
WithMetaWriter(wb.metaWriter).
WithMetaCache(wb.metaCache).
WithSchema(schema).
WithSyncPack(pack).
WithStorageConfig(packed.CreateStorageConfig()).
// The flush write path must keep retrying: aborting surfaces the error
// to SyncTask.HandleError, whose default callback panics the datanode.
// retry.Do short-circuits InputError-typed errors unless an explicit
// RetryErr predicate is supplied, so AttemptAlways alone is not enough.
WithWriteRetryOptions(retry.AttemptAlways(), retry.MaxSleepTime(10*time.Second),
retry.RetryErr(func(error) bool { return true }))
return task, nil
}
func (wb *writeBufferBase) getGrowingSourceSyncTask(ctx context.Context, segmentInfo *metacache.SegmentInfo, progress *growingSourceProgress) (syncmgr.Task, error) {
targetOffset := progress.targetOffset
pendingCommitted := progress.pendingCommitted
if pendingCommitted != nil {
targetOffset = pendingCommitted.targetOffset
}
checkpoint := progress.checkpointFor(targetOffset)
startPos := progress.firstUncommittedPosition()
if checkpoint == nil {
checkpoint = startPos
}
if checkpoint == nil {
checkpoint = wb.checkpoint
}
schemaTimestamp := uint64(0)
if startPos != nil {
schemaTimestamp = startPos.GetTimestamp()
}
var source syncmgr.GrowingFlushSource
if pendingCommitted == nil {
var state syncmgr.GrowingSourceState
source, state = wb.getGrowingSource(progress.segmentID, targetOffset, checkpoint)
if state != syncmgr.GrowingSourceUsable {
if source != nil {
source.Release()
}
return nil, errors.Wrapf(errGrowingSourceUnavailable, "segment %d state %d", progress.segmentID, state)
}
} else {
var state syncmgr.GrowingSourceState
source, state = wb.getGrowingSource(progress.segmentID, targetOffset, checkpoint)
if state != syncmgr.GrowingSourceUsable {
if source != nil {
source.Release()
source = nil
}
wb.logger.Warn(ctx, "growing source unavailable during committed flush ack retry; retrying SaveBinlogPaths without re-flush",
mlog.Int64("segmentID", progress.segmentID),
mlog.Int64("targetOffset", targetOffset),
mlog.Int("state", int(state)))
}
}
batchSize := targetOffset - segmentInfo.FlushedRows() - segmentInfo.SyncingRows()
buildTask := func(batchRows int64) *syncmgr.GrowingSourceSyncTask {
task := syncmgr.NewGrowingSourceSyncTask().
WithCollectionID(wb.collectionID).
WithPartitionID(segmentInfo.PartitionID()).
WithSegmentID(progress.segmentID).
WithChannelName(wb.channelName).
WithStartPosition(startPos).
WithCheckpoint(checkpoint).
WithBatchRows(batchRows).
WithTargetOffset(targetOffset).
WithLevel(segmentInfo.Level()).
WithMetaCache(wb.metaCache).
WithMetaWriter(wb.metaWriter).
WithSchema(wb.metaCache.GetSchema(schemaTimestamp)).
WithAllocator(wb.allocator).
WithFailureCallback(wb.errHandler).
// Same as above: keep the critical write path retrying despite the
// retry.Do InputError short-circuit.
WithWriteRetryOptions(retry.AttemptAlways(), retry.MaxSleepTime(10*time.Second),
retry.RetryErr(func(error) bool { return true }))
if source != nil {
task.WithSource(source)
}
if pendingCommitted != nil {
task.WithCommittedFlush(pendingCommitted.manifestPath, cloneBM25StatsMap(pendingCommitted.bm25Stats), pendingCommitted.insertBinlogs)
}
if segmentInfo.State() == commonpb.SegmentState_Flushing {
task.WithFlush()
}
if segmentInfo.State() == commonpb.SegmentState_Dropped {
task.WithDrop()
}
return task
}
if batchSize <= 0 {
progress.syncing = true
progress.syncingOffset = targetOffset
return buildTask(0), nil
}
if startPos != nil {
wb.syncCheckpoint.Add(progress.segmentID, startPos, "growing source syncing task")
}
progress.syncing = true
progress.syncingOffset = targetOffset
wb.metaCache.UpdateSegments(metacache.StartSyncing(batchSize), metacache.WithSegmentIDs(progress.segmentID))
return buildTask(batchSize), nil
}
// getEstBatchSize returns the batch size based on estimated size per record and FlushBufferSize configuration value.
func (wb *writeBufferBase) getEstBatchSize() uint {
sizeLimit := paramtable.Get().DataNodeCfg.FlushInsertBufferSize.GetAsInt64()
return uint(sizeLimit / int64(wb.estSizePerRecord))
}
func (wb *writeBufferBase) Close(ctx context.Context, drop bool) {
// sink all data and call Drop for meta writer
wb.mut.Lock()
wb.closed = true
wb.growingSourceRetryScheduled = false
if wb.growingSourceRetryTimer != nil {
wb.growingSourceRetryTimer.Stop()
wb.growingSourceRetryTimer = nil
}
if !drop {
wb.mut.Unlock()
return
}
var syncTasks []syncmgr.Task
segmentIDs := typeutil.NewSet[int64]()
for id := range wb.buffers {
segmentIDs.Insert(id)
}
for id := range wb.growingSourceProgress {
segmentIDs.Insert(id)
}
for _, id := range segmentIDs.Collect() {
syncTask, err := wb.getSyncTask(ctx, id)
if err != nil {
if wb.hasGrowingSourceProgress(id) {
mlog.Warn(ctx, "skip growing source sync while dropping write buffer",
mlog.Int64("segmentID", id),
mlog.String("channel", wb.channelName),
mlog.Err(err))
delete(wb.growingSourceProgress, id)
// flushSourceMode lives on metacache.SegmentInfo and is
// reclaimed when the segment is removed from metacache by
// the drop path (no manual cleanup needed here).
}
continue
}
switch t := syncTask.(type) {
case *syncmgr.SyncTask:
t.WithDrop()
case *syncmgr.GrowingSourceSyncTask:
t.WithDrop()
}
syncTasks = append(syncTasks, syncTask)
}
wb.mut.Unlock()
futures := wb.submitDropSyncTasks(ctx, syncTasks)
err := conc.AwaitAll(futures...)
if err != nil {
mlog.Error(ctx, "failed to sink write buffer data", mlog.Err(err))
// TODO change to remove channel in the future
panic(err)
}
err = wb.metaWriter.DropChannel(ctx, wb.channelName)
if err != nil {
mlog.Error(ctx, "failed to drop channel", mlog.Err(err))
// TODO change to remove channel in the future
panic(err)
}
}
func (wb *writeBufferBase) submitDropSyncTasks(ctx context.Context, syncTasks []syncmgr.Task) []*conc.Future[struct{}] {
futures := make([]*conc.Future[struct{}], 0, len(syncTasks))
for _, syncTask := range syncTasks {
f, err := wb.syncMgr.SyncData(ctx, syncTask, func(err error) error {
if wb.taskObserverCallback != nil {
wb.taskObserverCallback(syncTask, err)
}
if err != nil {
return err
}
if syncTask.StartPosition() != nil {
wb.mut.Lock()
wb.syncCheckpoint.Remove(syncTask.SegmentID(), syncTask.StartPosition().GetTimestamp())
wb.mut.Unlock()
}
return nil
})
if err != nil {
if growingSourceTask, ok := syncTask.(*syncmgr.GrowingSourceSyncTask); ok {
growingSourceTask.ReleaseSource()
}
mlog.Fatal(ctx, "failed to sync segment", mlog.Int64("segmentID", syncTask.SegmentID()), mlog.Err(err))
}
futures = append(futures, f)
}
return futures
}
// prepareInsert transfers InsertMsg into organized InsertData grouped by segmentID
// also returns primary key field data
func PrepareInsert(collSchema *schemapb.CollectionSchema, pkField *schemapb.FieldSchema, insertMsgs []*msgstream.InsertMsg) ([]*InsertData, error) {
bm25OutputFieldIDs, err := getBM25OutputFieldIDs(collSchema)
if err != nil {
return nil, err
}
groups := lo.GroupBy(insertMsgs, func(msg *msgstream.InsertMsg) int64 { return msg.SegmentID })
segmentPartition := lo.SliceToMap(insertMsgs, func(msg *msgstream.InsertMsg) (int64, int64) { return msg.GetSegmentID(), msg.GetPartitionID() })
result := make([]*InsertData, 0, len(groups))
for segment, msgs := range groups {
inData := &InsertData{
segmentID: segment,
partitionID: segmentPartition[segment],
data: make([]*storage.InsertData, 0, len(msgs)),
pkField: make([]storage.FieldData, 0, len(msgs)),
}
switch pkField.GetDataType() {
case schemapb.DataType_Int64:
inData.intPKTs = make(map[int64]int64)
case schemapb.DataType_VarChar:
inData.strPKTs = make(map[string]int64)
}
for _, msg := range msgs {
data, err := storage.InsertMsgToInsertData(msg, collSchema)
if err != nil {
mlog.Warn(context.TODO(), "failed to transfer insert msg to insert data", mlog.Err(err))
return nil, err
}
if len(bm25OutputFieldIDs) > 0 {
if inData.bm25Stats == nil {
inData.bm25Stats = make(map[int64]*storage.BM25Stats)
}
if err := appendBM25StatsFromInsertData(inData.bm25Stats, bm25OutputFieldIDs, data); err != nil {
return nil, err
}
}
pkFieldData, err := storage.GetPkFromInsertData(collSchema, data)
if err != nil {
return nil, err
}
if pkFieldData.RowNum() != data.GetRowNum() {
return nil, merr.WrapErrServiceInternal("pk column row num not match")
}
tsFieldData, err := storage.GetTimestampFromInsertData(data)
if err != nil {
return nil, err
}
if tsFieldData.RowNum() != data.GetRowNum() {
return nil, merr.WrapErrServiceInternal("timestamp column row num not match")
}
timestamps := tsFieldData.GetDataRows().([]int64)
switch pkField.GetDataType() {
case schemapb.DataType_Int64:
pks := pkFieldData.GetDataRows().([]int64)
for idx, pk := range pks {
ts, ok := inData.intPKTs[pk]
if !ok || timestamps[idx] < ts {
inData.intPKTs[pk] = timestamps[idx]
}
}
case schemapb.DataType_VarChar:
pks := pkFieldData.GetDataRows().([]string)
for idx, pk := range pks {
ts, ok := inData.strPKTs[pk]
if !ok || timestamps[idx] < ts {
inData.strPKTs[pk] = timestamps[idx]
}
}
}
inData.data = append(inData.data, data)
inData.pkField = append(inData.pkField, pkFieldData)
inData.tsField = append(inData.tsField, tsFieldData)
inData.rowNum += int64(data.GetRowNum())
}
result = append(result, inData)
}
return result, nil
}
func getBM25OutputFieldIDs(schema *schemapb.CollectionSchema) ([]int64, error) {
outputFieldIDs := make([]int64, 0)
for _, fn := range schema.GetFunctions() {
if fn.GetType() != schemapb.FunctionType_BM25 {
continue
}
outputField := typeutil.GetFunctionOutputField(schema, fn)
if outputField == nil {
return nil, merr.WrapErrFunctionFailedMsg("function %s output field not found", fn.GetName())
}
outputFieldIDs = append(outputFieldIDs, outputField.GetFieldID())
}
return outputFieldIDs, nil
}
func appendBM25StatsFromInsertData(stats map[int64]*storage.BM25Stats, outputFieldIDs []int64, data *storage.InsertData) error {
for _, outputFieldID := range outputFieldIDs {
outputData, ok := data.Data[outputFieldID]
if !ok {
return merr.WrapErrFunctionFailedMsg("BM25 output field %d not found in insert data", outputFieldID)
}
sparseData, ok := outputData.(*storage.SparseFloatVectorFieldData)
if !ok {
return merr.WrapErrFunctionFailedMsg("BM25 output field %d is not sparse vector data", outputFieldID)
}
if _, ok := stats[outputFieldID]; !ok {
stats[outputFieldID] = storage.NewBM25Stats()
}
stats[outputFieldID].AppendBytes(sparseData.GetContents()...)
}
return nil
}