mirror of
https://gitee.com/milvus-io/milvus.git
synced 2026-02-04 11:18:44 +08:00
If collection TTL property is malformed (e.g., non-numeric value), compaction tasks would fail silently and get stuck. This change: - Add centralized GetCollectionTTL/GetCollectionTTLFromMap functions in pkg/common to handle TTL parsing with proper error handling - Validate TTL property in createCollectionTask and alterCollectionTask PreExecute to reject invalid values early - Refactor datacoord compaction policies to use the new common functions - Remove duplicated getCollectionTTL from datacoord/util.go issue: #46716 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> - Core invariant: collection.ttl.seconds must be a parseable int64 and validated at collection creation/alter time so malformed TTLs never reach compaction/execution codepaths. - Bug fix (resolves #46716): malformed/non-numeric TTLs could silently cause compaction tasks to fail/stall; fixed by adding centralized parsing helpers pkg/common.GetCollectionTTL and GetCollectionTTLFromMap and validating TTL in createCollectionTask.PreExecute and alterCollectionTask.PreExecute (calls with default -1 and return parameter-invalid errors on parse failure). - Simplification / removed redundancy: eliminated duplicated getCollectionTTL in internal/datacoord/util.go and replaced ad-hoc TTL parsing across datacoord (compaction policies, import_util, compaction triggers) and proxy util with the common helpers, centralizing error handling and defaulting logic. - No data loss or behavior regression: valid TTL parsing semantics unchanged (helpers use identical int64 parsing and default fallback from paramtable/CommonCfg); validation occurs in PreExecute so existing valid collections proceed unchanged while malformed values are rejected early—compaction codepaths now receive only validated TTL values (or explicit defaults), preventing silent skips without altering valid execution flows. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Congqi Xia <congqi.xia@zilliz.com>
345 lines
11 KiB
Go
345 lines
11 KiB
Go
// Licensed to the LF AI & Data foundation under one
|
|
// or more contributor license agreements. See the NOTICE file
|
|
// distributed with this work for additional information
|
|
// regarding copyright ownership. The ASF licenses this file
|
|
// to you under the Apache License, Version 2.0 (the
|
|
// "License"); you may not use this file except in compliance
|
|
// with the License. You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package datacoord
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/samber/lo"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/milvus-io/milvus/internal/datacoord/allocator"
|
|
"github.com/milvus-io/milvus/pkg/v2/common"
|
|
"github.com/milvus-io/milvus/pkg/v2/log"
|
|
"github.com/milvus-io/milvus/pkg/v2/proto/datapb"
|
|
"github.com/milvus-io/milvus/pkg/v2/util/merr"
|
|
"github.com/milvus-io/milvus/pkg/v2/util/paramtable"
|
|
)
|
|
|
|
// singleCompactionPolicy is to compact one segment with too many delta logs
|
|
// support l2 single segment only for now
|
|
// todo: move l1 single compaction here
|
|
type singleCompactionPolicy struct {
|
|
meta *meta
|
|
allocator allocator.Allocator
|
|
handler Handler
|
|
}
|
|
|
|
func newSingleCompactionPolicy(meta *meta, allocator allocator.Allocator, handler Handler) *singleCompactionPolicy {
|
|
return &singleCompactionPolicy{meta: meta, allocator: allocator, handler: handler}
|
|
}
|
|
|
|
func (policy *singleCompactionPolicy) Enable() bool {
|
|
return Params.DataCoordCfg.EnableAutoCompaction.GetAsBool()
|
|
}
|
|
|
|
func (policy *singleCompactionPolicy) Trigger(ctx context.Context) (map[CompactionTriggerType][]CompactionView, error) {
|
|
collections := policy.meta.GetCollections()
|
|
|
|
events := make(map[CompactionTriggerType][]CompactionView, 0)
|
|
views := make([]CompactionView, 0)
|
|
sortViews := make([]CompactionView, 0)
|
|
partitionKeySortViews := make([]CompactionView, 0)
|
|
for _, collection := range collections {
|
|
collectionViews, collectionSortViews, _, err := policy.triggerOneCollection(ctx, collection.ID, false)
|
|
if err != nil {
|
|
// not throw this error because no need to fail because of one collection
|
|
log.Warn("fail to trigger single compaction", zap.Int64("collectionID", collection.ID), zap.Error(err))
|
|
}
|
|
views = append(views, collectionViews...)
|
|
if IsPartitionKeySortCompactionEnabled(collection.Properties) {
|
|
partitionKeySortViews = append(partitionKeySortViews, collectionSortViews...)
|
|
} else {
|
|
sortViews = append(sortViews, collectionSortViews...)
|
|
}
|
|
}
|
|
events[TriggerTypeSingle] = views
|
|
events[TriggerTypeSort] = sortViews
|
|
events[TriggerTypePartitionKeySort] = partitionKeySortViews
|
|
return events, nil
|
|
}
|
|
|
|
func (policy *singleCompactionPolicy) triggerSegmentSortCompaction(
|
|
ctx context.Context,
|
|
segmentID int64,
|
|
) CompactionView {
|
|
log := log.With(zap.Int64("segmentID", segmentID))
|
|
if !Params.DataCoordCfg.EnableSortCompaction.GetAsBool() {
|
|
log.RatedInfo(20, "stats task disabled, skip sort compaction")
|
|
return nil
|
|
}
|
|
segment := policy.meta.GetHealthySegment(ctx, segmentID)
|
|
if segment == nil {
|
|
log.Warn("fail to apply triggerSegmentSortCompaction, segment not healthy")
|
|
return nil
|
|
}
|
|
|
|
collection, err := policy.handler.GetCollection(ctx, segment.GetCollectionID())
|
|
if err != nil {
|
|
log.Warn("fail to apply triggerSegmentSortCompaction, unable to get collection from handler",
|
|
zap.Error(err))
|
|
return nil
|
|
}
|
|
if collection == nil {
|
|
log.Warn("fail to apply triggerSegmentSortCompaction, collection not exist")
|
|
return nil
|
|
}
|
|
isPartitionIsolationEnabled := IsPartitionKeySortCompactionEnabled(collection.Properties)
|
|
if !canTriggerSortCompaction(segment, isPartitionIsolationEnabled) {
|
|
log.Warn("fail to apply triggerSegmentSortCompaction",
|
|
zap.String("state", segment.GetState().String()),
|
|
zap.String("level", segment.GetLevel().String()),
|
|
zap.Bool("isSorted", segment.GetIsSorted()),
|
|
zap.Bool("isImporting", segment.GetIsImporting()),
|
|
zap.Bool("isCompacting", segment.isCompacting),
|
|
zap.Bool("isInvisible", segment.GetIsInvisible()))
|
|
return nil
|
|
}
|
|
|
|
collectionTTL, err := common.GetCollectionTTLFromMap(collection.Properties, paramtable.Get().CommonCfg.EntityExpirationTTL.GetAsDuration(time.Second))
|
|
if err != nil {
|
|
log.Warn("failed to apply triggerSegmentSortCompaction, get collection ttl failed")
|
|
return nil
|
|
}
|
|
|
|
newTriggerID, err := policy.allocator.AllocID(ctx)
|
|
if err != nil {
|
|
log.Warn("fail to apply triggerSegmentSortCompaction, unable to allocate triggerID", zap.Error(err))
|
|
return nil
|
|
}
|
|
|
|
segmentViews := GetViewsByInfo(segment)
|
|
view := &MixSegmentView{
|
|
label: segmentViews[0].label,
|
|
segments: segmentViews,
|
|
collectionTTL: collectionTTL,
|
|
triggerID: newTriggerID,
|
|
}
|
|
|
|
log.Info("succeeded to apply triggerSegmentSortCompaction",
|
|
zap.Int64("triggerID", newTriggerID))
|
|
return view
|
|
}
|
|
|
|
func IsPartitionKeySortCompactionEnabled(properties map[string]string) bool {
|
|
iso, _ := common.IsPartitionKeyIsolationPropEnabled(properties)
|
|
return Params.CommonCfg.EnableNamespace.GetAsBool() && iso
|
|
}
|
|
|
|
func (policy *singleCompactionPolicy) triggerSortCompaction(
|
|
ctx context.Context,
|
|
triggerID int64,
|
|
collectionID int64,
|
|
collectionTTL time.Duration,
|
|
) ([]CompactionView, error) {
|
|
log := log.With(zap.Int64("collectionID", collectionID))
|
|
if !Params.DataCoordCfg.EnableSortCompaction.GetAsBool() {
|
|
log.RatedInfo(20, "stats task disabled, skip sort compaction")
|
|
return nil, nil
|
|
}
|
|
views := make([]CompactionView, 0)
|
|
|
|
collection, err := policy.handler.GetCollection(ctx, collectionID)
|
|
if err != nil {
|
|
log.Warn("fail to apply triggerSegmentSortCompaction, unable to get collection from handler",
|
|
zap.Error(err))
|
|
return nil, err
|
|
}
|
|
if collection == nil {
|
|
log.Warn("fail to apply triggerSegmentSortCompaction, collection not exist")
|
|
return nil, merr.WrapErrCollectionNotFound(collectionID)
|
|
}
|
|
isPartitionIsolationEnabled := IsPartitionKeySortCompactionEnabled(collection.Properties)
|
|
triggerableSegments := policy.meta.SelectSegments(ctx, WithCollection(collectionID),
|
|
SegmentFilterFunc(func(seg *SegmentInfo) bool {
|
|
return canTriggerSortCompaction(seg, isPartitionIsolationEnabled)
|
|
}))
|
|
if len(triggerableSegments) == 0 {
|
|
log.RatedInfo(20, "no triggerable segments")
|
|
return views, nil
|
|
}
|
|
|
|
gbSegments := lo.GroupBy(triggerableSegments, func(seg *SegmentInfo) bool {
|
|
return seg.GetIsInvisible()
|
|
})
|
|
invisibleSegments, ok := gbSegments[true]
|
|
if ok {
|
|
for _, segment := range invisibleSegments {
|
|
segmentViews := GetViewsByInfo(segment)
|
|
view := &MixSegmentView{
|
|
label: segmentViews[0].label,
|
|
segments: segmentViews,
|
|
collectionTTL: collectionTTL,
|
|
triggerID: triggerID,
|
|
}
|
|
views = append(views, view)
|
|
}
|
|
}
|
|
|
|
visibleSegments, ok := gbSegments[false]
|
|
if ok {
|
|
for i, segment := range visibleSegments {
|
|
if i > Params.DataCoordCfg.SortCompactionTriggerCount.GetAsInt() {
|
|
break
|
|
}
|
|
segmentViews := GetViewsByInfo(segment)
|
|
view := &MixSegmentView{
|
|
label: segmentViews[0].label,
|
|
segments: segmentViews,
|
|
collectionTTL: collectionTTL,
|
|
triggerID: triggerID,
|
|
}
|
|
views = append(views, view)
|
|
}
|
|
}
|
|
|
|
log.Info("succeeded to apply triggerSortCompaction",
|
|
zap.Int64("triggerID", triggerID),
|
|
zap.Int("triggered view num", len(views)))
|
|
return views, nil
|
|
}
|
|
|
|
func (policy *singleCompactionPolicy) triggerOneCollection(ctx context.Context, collectionID int64, manual bool) ([]CompactionView, []CompactionView, int64, error) {
|
|
log := log.With(zap.Int64("collectionID", collectionID))
|
|
collection, err := policy.handler.GetCollection(ctx, collectionID)
|
|
if err != nil {
|
|
log.Warn("fail to apply singleCompactionPolicy, unable to get collection from handler",
|
|
zap.Error(err))
|
|
return nil, nil, 0, err
|
|
}
|
|
if collection == nil {
|
|
log.Warn("fail to apply singleCompactionPolicy, collection not exist")
|
|
return nil, nil, 0, nil
|
|
}
|
|
|
|
collectionTTL, err := common.GetCollectionTTLFromMap(collection.Properties, paramtable.Get().CommonCfg.EntityExpirationTTL.GetAsDuration(time.Second))
|
|
if err != nil {
|
|
log.Warn("failed to apply singleCompactionPolicy, get collection ttl failed")
|
|
return nil, nil, 0, err
|
|
}
|
|
|
|
newTriggerID, err := policy.allocator.AllocID(ctx)
|
|
if err != nil {
|
|
log.Warn("fail to apply singleCompactionPolicy, unable to allocate triggerID", zap.Error(err))
|
|
return nil, nil, 0, err
|
|
}
|
|
|
|
sortViews, err := policy.triggerSortCompaction(ctx, newTriggerID, collectionID, collectionTTL)
|
|
if err != nil {
|
|
log.Warn("failed to apply singleCompactionPolicy, trigger sort compaction failed", zap.Error(err))
|
|
return nil, nil, 0, err
|
|
}
|
|
if !isCollectionAutoCompactionEnabled(collection) {
|
|
log.RatedInfo(20, "collection auto compaction disabled")
|
|
return nil, sortViews, 0, nil
|
|
}
|
|
|
|
views := make([]CompactionView, 0)
|
|
partSegments := GetSegmentsChanPart(policy.meta, collectionID, SegmentFilterFunc(func(segment *SegmentInfo) bool {
|
|
return isSegmentHealthy(segment) &&
|
|
isFlushed(segment) &&
|
|
!segment.isCompacting && // not compacting now
|
|
!segment.GetIsImporting() && // not importing now
|
|
segment.GetLevel() == datapb.SegmentLevel_L2 && // only support L2 for now
|
|
!segment.GetIsInvisible()
|
|
}))
|
|
|
|
for _, group := range partSegments {
|
|
if Params.DataCoordCfg.IndexBasedCompaction.GetAsBool() {
|
|
group.segments = FilterInIndexedSegments(ctx, policy.handler, policy.meta, false, group.segments...)
|
|
}
|
|
|
|
for _, segment := range group.segments {
|
|
if hasTooManyDeletions(segment) {
|
|
segmentViews := GetViewsByInfo(segment)
|
|
view := &MixSegmentView{
|
|
label: segmentViews[0].label,
|
|
segments: segmentViews,
|
|
collectionTTL: collectionTTL,
|
|
triggerID: newTriggerID,
|
|
}
|
|
views = append(views, view)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(views) > 0 {
|
|
log.Info("succeeded to apply singleCompactionPolicy",
|
|
zap.Int64("triggerID", newTriggerID),
|
|
zap.Int("triggered view num", len(views)))
|
|
}
|
|
return views, sortViews, newTriggerID, nil
|
|
}
|
|
|
|
var _ CompactionView = (*MixSegmentView)(nil)
|
|
|
|
type MixSegmentView struct {
|
|
label *CompactionGroupLabel
|
|
segments []*SegmentView
|
|
collectionTTL time.Duration
|
|
triggerID int64
|
|
}
|
|
|
|
func (v *MixSegmentView) GetGroupLabel() *CompactionGroupLabel {
|
|
if v == nil {
|
|
return &CompactionGroupLabel{}
|
|
}
|
|
return v.label
|
|
}
|
|
|
|
func (v *MixSegmentView) GetSegmentsView() []*SegmentView {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
|
|
return v.segments
|
|
}
|
|
|
|
func (v *MixSegmentView) Append(segments ...*SegmentView) {
|
|
if v.segments == nil {
|
|
v.segments = segments
|
|
return
|
|
}
|
|
|
|
v.segments = append(v.segments, segments...)
|
|
}
|
|
|
|
func (v *MixSegmentView) String() string {
|
|
strs := lo.Map(v.segments, func(segView *SegmentView, _ int) string {
|
|
return segView.String()
|
|
})
|
|
return fmt.Sprintf("label=<%s>, segments=%v", v.label.String(), strs)
|
|
}
|
|
|
|
func (v *MixSegmentView) Trigger() (CompactionView, string) {
|
|
return v, ""
|
|
}
|
|
|
|
func (v *MixSegmentView) ForceTrigger() (CompactionView, string) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (v *MixSegmentView) ForceTriggerAll() ([]CompactionView, string) {
|
|
panic("implement me")
|
|
}
|
|
|
|
func (v *MixSegmentView) GetTriggerID() int64 {
|
|
return v.triggerID
|
|
}
|