Files
milvus/internal/flushcommon/writebuffer/write_buffer.go
T
Jamesandxiaofanluan 37f3a829e7 fix: make local storage keys complete paths at every layer (#53145)
issue: #53051, #53052, #53152

## Summary

Align local storage around one path contract: a key is a complete
absolute filesystem path, and the local loon/Arrow filesystem is rooted
at `/`. Previously, some writers used `minio.rootPath` while others
supplied complete local paths to an already-prefixed filesystem, placing
files in different physical locations from those expected by readers and
GC.

Remote keys remain complete bucket-relative object keys; their
filesystem-root convention is unchanged.

## Changes

- Unify Go/C++ local filesystem roots and StorageV3 manifest-base
construction across writers. Preserve the existing base/version pair
when continuing a growing-source manifest.
- Fix local JSON/TEXT stats reads, rejected-stats cleanup, and explore
temporary paths. Exercise resolved V3 stats paths through the actual
`LocalChunkManager.MultiRead` interface.
- Resolve legacy local manifest bases in loaded metadata and locally
referenced snapshots, without eager catalog or manifest-file writes.
Preserve manifest CAS compatibility and pathless V3 summaries;
copy/restore writes use the target storage root.
- Require an absolute, nonempty `localStorage.path` at initialization,
including on remote deployments where it is a cache directory. Freeze
forbidden parameters against configuration-source refresh while
retaining explicit runtime/test overrides, and derive mmap, pprof, and
disk-capacity defaults from the same canonical startup value.
- Make segment deletion prefixes segment-boundary aware, retain
root/identity and snapshot checks, and add BM25 orphan scanning. These
shared GC changes also apply to remote storage; this PR does not claim
every remote code path is untouched.
- Remove the local test-only root override and unused local-only
property initialization chain; use the production storage-config
initialization path.

## Automatic local upgrade

Let `R = localStorage.path` and `M = minio.rootPath`. Before standalone
components start, local deployments check the known legacy locations:

| Existing data | Upgrade handling |
| --- | --- |
| Canonical files under `R/<layout>` | Leave in place. |
| Double-root files under `R/<R-without-leading-slash>/<layout>` | Move
into `R/<layout>` for `insert_log`, `text_log`, `json_stats`,
`index_files`, and `index_v1`. |
| Recognized late-2.6 unified packed indexes under the process CWD |
Move into the corresponding directory under `R`, if visible to the new
process. |
| Legacy manifest base `M/insert_log/<c>/<p>/<s>`, with files under
`R/M/insert_log/<c>/<p>/<s>` | **Do not move the files.** Resolve the
loaded manifest base to that existing absolute location. New segment
layouts use the canonical root; updates to an existing legacy manifest
can retain its base. |

Migration prefers directory/subtree rename and recursively merges
overlapping directories. Cross-filesystem moves fall back to
copy-then-remove. Existing same-name files cause a conflict rather than
content comparison or overwrite, except a published copy recognized as
this migration's own interrupted work. The copy path synchronizes the
file and target directory chain before deleting the source.

Remaining sources are rediscovered on retry; empty legacy directories
are pruned. Once migrated, restarts probe the known old locations
without rescanning canonical data. The initial pass walks legacy file
metadata but does not hash file contents. There is no configured
migration timeout. SIGTERM/SIGINT cancels the migration and prevents
etcd or components from starting; directory walks and cross-filesystem
copies observe cancellation between filesystem reads. A canceled partial
copy is never published and is recopied from the intact source on the
next start.

No user migration script, offline command, reverse migration, journal,
or backup is provided. Successfully moved sources are removed.

## Compatibility and limits

- Preserve the absolute local root, data mounts, and legacy
`minio.rootPath` when upgrading. Canonical 2.6 data stays in place;
misplaced late-2.6 indexes and affected 3.0.0/3.0.1 layouts are handled
as above. Relative roots from older versions are unsupported.
- Automatic migration is standalone/local-only. If old CWD indexes
disappeared with an old container, they cannot be discovered:
preserve/make those files visible or rebuild the derived indexes.
Cross-filesystem copy has no configured timeout and can extend the first
startup, but a shutdown signal cancels it and the next start resumes
from the remaining source.
- Downgrade to the affected old layout is unsupported after migration.
Symlink layouts require manual resolution.
- Known legacy LOB/orphan GC gaps under `R/M/insert_log`, pending old
external-refresh paths, and rare interrupted-copy publication residue
remain outside scope. This is not a claim that every historical file is
recovered or reclaimable.

## Validation

At head `898dba4b41`, `ci-v2/build-ut-cov` passed Build, Code Check,
Integration UT, Go UT, and C++ UT (9,145/9,145). Diff coverage was 100%
for C++ and 81.2% for Go; total patch coverage was 82.8%.

Focused local tests for canonical/frozen `localStorage.path` derivation
and migration cancellation/deadline/copy recovery passed with `-tags
dynamic,test -gcflags='all=-N -l' -count=1`, and `git diff --check`
passed before publication. Full paramtable execution remains locally
blocked by tests attempting to create `/var/lib/milvus/data`;
roles/native-dependent Go tests remain build-blocked by local C++
header/library mismatches. A real 3.0.0/3.0.1 version-to-version upgrade
was not executed locally. `ci-v2/e2e-default` and `ci-v2/go-sdk` are
still pending for this head.

Signed-off-by: xiaofanluan <[email protected]>
Co-authored-by: xiaofanluan <[email protected]>
2026-09-17 09:43:08 +08:00

1828 lines
61 KiB
Go

package writebuffer
import (
"context"
"fmt"
"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/internal/util/streamingutil"
"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/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(info CreateGrowingSegmentInfo) error
// 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)
// AllowGrowingSourceFlush returns true if this write buffer may try growing-source flush.
AllowGrowingSourceFlush() 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. Otherwise,
// tracked growing-source segments are added to the requested segmentIDs.
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 CreateGrowingSegmentInfo struct {
PartitionID int64
SegmentID int64
StartPos *msgpb.MsgPosition
SchemaVersion int32
StorageVersion int64
}
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
pkStats *storage.PrimaryKeyStats
}
// 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.
// Channel-level admission flag for trying growing-source flush. Actual segment
// source selection remains sticky in metacache.
allowGrowingSourceFlush 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
}
allowGrowingSourceFlush := typeutil.AllowGrowingSourceFlush(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,
allowGrowingSourceFlush: allowGrowingSourceFlush,
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) AllowGrowingSourceFlush() bool {
return wb.allowGrowingSourceFlush
}
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)
} else {
segmentIDs = lo.Uniq(append(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 {
if seg.GetStorageVersion() != storage.StorageV3 {
return growingFlushSourceDecision{sourceType: metacache.FlushSourceWriteBuffer}
}
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.allowGrowingSourceFlush {
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) error {
err := wb.CreateNewGrowingSegment(CreateGrowingSegmentInfo{
PartitionID: inData.partitionID,
SegmentID: inData.segmentID,
StartPos: startPos,
SchemaVersion: schemaVersion,
})
if err != nil {
return err
}
segment, ok := wb.metaCache.GetSegmentByID(inData.segmentID)
if !ok {
return merr.WrapErrSegmentNotFound(inData.segmentID)
}
if segment.GetStorageVersion() != storage.StorageV3 {
return merr.WrapErrServiceInternalMsg("growing-source flush requires StorageV3 segment, segmentID=%d storageVersion=%d",
inData.segmentID, segment.GetStorageVersion())
}
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)
return nil
}
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.allowGrowingSourceFlush {
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(),
pkStats: growingSourceTask.CommittedPKStats(),
}
}
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.allowGrowingSourceFlush {
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) GetBM25Stats() map[int64]*storage.BM25Stats {
return id.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(info CreateGrowingSegmentInfo) error {
_, ok := wb.metaCache.GetSegmentByID(info.SegmentID)
// new segment
if !ok {
storageVersion, err := wb.resolveNewGrowingSegmentStorageVersion(info)
if err != nil {
return err
}
manifestPath := wb.newGrowingSegmentManifestPath(info.PartitionID, info.SegmentID, storageVersion)
segmentInfo := &datapb.SegmentInfo{
ID: info.SegmentID,
PartitionID: info.PartitionID,
CollectionID: wb.collectionID,
InsertChannel: wb.channelName,
StartPosition: info.StartPos,
State: commonpb.SegmentState_Growing,
StorageVersion: storageVersion,
ManifestPath: manifestPath,
SchemaVersion: info.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(info.SegmentID), mlog.String("channel", wb.channelName), mlog.Int64("storage version", storageVersion))
}
return nil
}
func (wb *writeBufferBase) resolveNewGrowingSegmentStorageVersion(info CreateGrowingSegmentInfo) (int64, error) {
switch info.StorageVersion {
case storage.StorageV2, storage.StorageV3:
return info.StorageVersion, nil
case storage.StorageV1:
if streamingutil.IsStreamingServiceEnabled() {
return 0, merr.WrapErrServiceInternalMsg("missing storage version for streaming growing segment, segmentID=%d", info.SegmentID)
}
inferred := storage.StorageV2
reason := "default non-streaming storage version"
if typeutil.HasTextField(wb.metaCache.GetSchema(0)) {
inferred = storage.StorageV3
reason = "TEXT field requires StorageV3"
} else if paramtable.Get().CommonCfg.UseLoonFFI.GetAsBool() {
inferred = storage.StorageV3
reason = "common.storage.useLoonFFI enabled"
}
mlog.Warn(context.TODO(), "infer missing storage version for non-streaming growing segment",
mlog.FieldSegmentID(info.SegmentID),
mlog.Int64("collectionID", wb.collectionID),
mlog.String("channel", wb.channelName),
mlog.Int64("inferredStorageVersion", inferred),
mlog.String("reason", reason))
return inferred, nil
default:
return 0, merr.WrapErrServiceInternalMsg("unsupported storage version for growing segment, segmentID=%d storageVersion=%d",
info.SegmentID, info.StorageVersion)
}
}
func (wb *writeBufferBase) newGrowingSegmentManifestPath(partitionID int64, segmentID int64, storageVersion int64) string {
if storageVersion != storage.StorageV3 {
return ""
}
basePath := storage.SegmentManifestBasePath(packed.CreateStorageConfig().GetRootPath(), wb.collectionID, partitionID, segmentID)
return packed.MarshalManifestPath(basePath, packed.ManifestEarliest)
}
// 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) {
if segmentInfo.GetStorageVersion() != storage.StorageV3 {
return nil, merr.WrapErrServiceInternalMsg("growing-source sync requires StorageV3 segment, segmentID=%d storageVersion=%d",
segmentInfo.SegmentID(), segmentInfo.GetStorageVersion())
}
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).
WithStorageConfig(packed.CreateStorageConfig()).
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)
task.WithCommittedPKStats(pendingCommitted.pkStats)
}
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
}