Files
milvus/internal/datacoord/meta_test.go
T
e2787d3981 enhance: standardize error handling on merr + Sys/Input classification (#50221)
issue: #47420

## What this PR does

Project-wide migration of raw `fmt.Errorf` / `errors.New` in function
bodies onto
the `merr` framework, plus the Sys-vs-Input error classification and the
machinery it drives (retriability, fine-grained metrics, segcore
unification),
plus the convention docs and a linter that keeps it from regressing.

Scope: storage, proxy, coordinators (root/data/query), query node, data
node,
`pkg/util` & `internal/util`, expression parser, message queue,
streaming, and
misc packages. Bare raw-error usages went from ~3000 to a ~340 allowlist
(package-level sentinels / build-tag / test sites).

---

## How to review this PR

It is large but the vast majority is mechanical. Changes fall into three
tiers;
spend review budget on Part 2 and Part 3.

### Part 1 — Mechanical standardization (low risk, verify by rule)

Each converted call follows one of a small fixed set of rules. To
review, check
that each site obeys the matching rule rather than reading every line:

| Pattern | Rule |
|---|---|
| `fmt.Errorf("...")` originating a new error | →
`merr.WrapErrXxxMsg("...")` with a code matching the failure's meaning |
| Adding context to an existing typed error | → `merr.Wrap(err, "...")`
/ `merr.Wrapf(...)` — **preserves** the inner code (never `WrapErr*Err`,
which overwrites it) |
| Errors inside the streaming subsystem | → `status.New*` factories
(StreamingError), **not** merr — this is the component-internal dialect
(see `docs/dev/error_handling_guide.md`) |
| Low-level / control-flow signal caught by `errors.Is` | → kept as a
package-level `errors.New` sentinel (lowercase, same-package) |

Conventions are documented in `docs/dev/error_handling_guide.md`
(how-to) and
`docs/dev/error_sentinel_convention.md` (rules + audit). A
`gocritic`/`ruleguard`
rule (`rawmerrerror`, in `rules.go`) enforces "no raw `return
errors.New/fmt.Errorf`"
under `make verifiers`.

### Part 2 — Behavior changes (review these closely)

These are the sites where the wire contract or runtime behavior changes,
not just
the source text. Listed by category; representative locations given,
full set in
the diff.

**A. gRPC wire-code shifts: `UnexpectedError(1)/Code 65535` → typed
code.**
Where a handler previously returned a raw error (collapsed to
`Code=65535` on the
wire), it now returns a typed merr, so the client sees a real code. The
most
common shift is to `IllegalArgument(5)/Code 1100` (ParameterInvalid).
Touch
points include datanode task handlers (CreateTask/Query/Drop), proxy
Upsert,
querynode GetMetrics, datacoord CreateIndex, httpserver query-response
builder,
and typeutil schema validation. One code refinement: an index-param
validation
moved `1100` → `1101` (ParameterMissing). **Client/SDK assertions and
any code
that switched on `Code=65535` for these paths must be re-checked** (the
go_client
e2e assertions were already aligned in this PR).

**B. Prometheus `status` label contract change (externally visible).**
The proxy metric's coarse `fail` / `rejected` values are split into
`fail_input` / `fail_system` and `rejected_user` / `rejected_system` (in
`requestutil.ParseMetricLabel`; auth/privilege rejections count as
`rejected_user`), so dashboards can attribute a failure to caller vs
operator.
**Dashboards/alerts querying `status="fail"` must migrate to
`status=~"fail_.*"`, and `status="rejected"` to
`status=~"rejected_.*"`.** The
in-repo Grafana dashboard is already migrated; external dashboards built
on the
old values silently go empty after upgrade. This is the one change that
requires an ops-side migration.

**C. Retriability semantics.**
- C1: `merr.Status(err)` now forces `Retriable=false` when the error is
an
`InputError` — a malformed request can never succeed on blind retry, so
clients
never get the self-contradictory "your input is wrong but you may
retry".
- C2: `retry.Do` short-circuits an `InputError` (non-retriable) — **but
only when
  the caller did not pass a `RetryErr` predicate**. The check is an
`if c.isRetryErr != nil { ... } else if InputError { ... }` *mutually
exclusive*
branch (`pkg/util/retry/retry.go`): an explicit `RetryErr` takes
precedence and
bypasses the InputError abort. `retry.Handle` deliberately does **not**
apply
the InputError abort (its callers signal abort via `shouldRetry=false`).
Four
flusher startup callsites that must retry through transient "not ready"
errors
  were given explicit `RetryErr` escape hatches.

**D. segcore (C++→Go) error classification.**
A single shared Go-side table (`pkg/util/merr/segcore.go`) maps each
segcore code
to a merr sentinel + InputError/signal category, replacing scattered
hand-written
`if errorCode == ...` switches in the cgo wrappers. **Wire `Code` values
change
for every segcore pass-through error, not just the remapped ones.**
Named
sentinels remap (C++ `2003` → merr `2001`, `2033` → `2002`,
Folly/Knowhere codes
likewise); **all remaining pass-through codes (`2004`–`2043`, previously
surfaced to clients as raw C++ enum values) now serialize as `2000`**
(`ErrSegcore`), with the original C++ code preserved in the `Reason`
text
(`segcoreCode=...`); unknown/future codes collapse to `2000` as well
(pinned by
the `wire_code_projection` test). Transient segcore classes (object
storage /
file IO / OOM / mmap / FieldNotLoaded — 11 codes) now report
`Retriable=true`.
**Any client switching on raw segcore codes in the `2004`–`2043` range
must be
re-checked**; the in-Reason code remains available for diagnostics.
Signal
codes (PretendFinished / FollyCancel) are recognized centrally.
`errors.Is`-based
control flow on these (e.g. scheduler skip/retry) is preserved.

**E. InputError classification (25 sentinels + dynamic marks).**
25 sentinels in `errors.go` carry `WithErrorType(InputError)` (the
Collection /
ResourceGroup / Database families, `ErrIndexDuplicate`,
`ErrParameterInvalid`,
`ErrPrivilegeNotAuthenticated`, `ErrImportFailed`, `ErrQueryPlan`, ...),
plus dynamic
marks for the 8 segcore input codes (ExprInvalid, DimNotMatch,
MetricTypeInvalid, FieldIDInvalid, ...) and
`WrapErrAsInputError`. The widest blast radius is `ErrParameterInvalid`
(1100):
~2335 `WrapErrParameterInvalid*` callsites now classify as input /
non-retriable. Because of C1/C2 this changes retriability for
any path that returns these. **The audit to confirm no transient path
was
mis-marked is the single most important review item** (see Part 3). One
reverse
correction: storage field-stats parsing moved from `ErrParameterInvalid`
(input)
to `ErrDataIntegrity` — a corrupted stored stat is data corruption, not
user
input.

### Part 3 — Known risks & traps (called out proactively)

1. **`merr.Wrap` vs `WrapErr*Err` (code-masking).** `WrapErr*Err` builds
a
`wrappedMilvusError{sentinel: ErrServiceInternal}` whose `code()`
returns the
*outer* sentinel — it overwrites the inner typed code and hides the
`errors.Is`
chain. This is intentional (use it to *deliberately* downgrade), but it
was a
recurring conversion defect; the rule "add context with `merr.Wrap`,
downgrade
with `WrapErr*Err`" is enforced by convention and reviewed across the
diff.
2. **InputError × `retry.Do` blast radius.** Marking a sentinel
`InputError` makes
any `retry.Do(...)` without a `RetryErr` predicate stop retrying it.
Reviewers
should sanity-check that no transient use of the 19 newly-marked
sentinels
(especially `ErrParameterInvalid`) sits inside a retry loop that needed
to keep
   spinning. The known flusher cases were handled (see C2).
3. **The ~340 raw-error allowlist.** What remains as bare `errors.New`
is, by
design: package-level sentinels (caught by `errors.Is`), `//go:build
test`
sites, and out-of-band trees (`cmd/`, `tests/`, codegen, walimpls). The
linter
only bans the *direct-return* form; assignment-then-return escapes and
the full
no-exceptions ban are deferred to an AST-based linter (Tier 2,
documented).
4. **segcore C++ second step deferred.** This PR unifies classification
on the Go
side; splitting the dual-semantic C++ codes at the source is a
follow-up.

---

## Validation

- `make verifiers`: Go side clean (gofmt + static-check across modules,
including
  the new `rawmerrerror` rule with a 0-hit baseline repo-wide).
- `make test-go`: passing; the one real regression introduced (a
datanode
`invalid_task_type` assertion shifting `1` → `5` from a ParameterInvalid
  conversion) was fixed in-tree.
- go_client e2e CreateIndex assertions aligned to the new merr messages.

---------

Signed-off-by: zhenshan.cao <zhenshan.cao@zilliz.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-12 15:04:51 -07:00

5319 lines
181 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"
"math/rand"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/bytedance/mockey"
"github.com/cockroachdb/errors"
prometheustestutil "github.com/prometheus/client_golang/prometheus/testutil"
"github.com/samber/lo"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"golang.org/x/exp/slices"
"google.golang.org/protobuf/proto"
"github.com/milvus-io/milvus-proto/go-api/v3/commonpb"
"github.com/milvus-io/milvus-proto/go-api/v3/milvuspb"
"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/datacoord/broker"
mockkv "github.com/milvus-io/milvus/internal/kv/mocks"
"github.com/milvus-io/milvus/internal/metastore"
"github.com/milvus-io/milvus/internal/metastore/kv/datacoord"
mocks2 "github.com/milvus-io/milvus/internal/metastore/mocks"
"github.com/milvus-io/milvus/internal/metastore/model"
"github.com/milvus-io/milvus/internal/mocks"
"github.com/milvus-io/milvus/internal/storage"
"github.com/milvus-io/milvus/internal/storagev2/packed"
kvfactory "github.com/milvus-io/milvus/internal/util/dependency/kv"
"github.com/milvus-io/milvus/pkg/v3/common"
"github.com/milvus-io/milvus/pkg/v3/kv"
"github.com/milvus-io/milvus/pkg/v3/metrics"
"github.com/milvus-io/milvus/pkg/v3/proto/datapb"
"github.com/milvus-io/milvus/pkg/v3/proto/indexpb"
"github.com/milvus-io/milvus/pkg/v3/proto/rootcoordpb"
"github.com/milvus-io/milvus/pkg/v3/util"
"github.com/milvus-io/milvus/pkg/v3/util/funcutil"
"github.com/milvus-io/milvus/pkg/v3/util/merr"
"github.com/milvus-io/milvus/pkg/v3/util/metricsinfo"
"github.com/milvus-io/milvus/pkg/v3/util/paramtable"
"github.com/milvus-io/milvus/pkg/v3/util/testutils"
"github.com/milvus-io/milvus/pkg/v3/util/typeutil"
)
// MetaReloadSuite tests meta reload & meta creation related logic
type MetaReloadSuite struct {
testutils.PromMetricsSuite
catalog *mocks2.DataCoordCatalog
meta *meta
}
func (suite *MetaReloadSuite) SetupTest() {
catalog := mocks2.NewDataCoordCatalog(suite.T())
suite.catalog = catalog
}
func (suite *MetaReloadSuite) resetMock() {
suite.catalog.ExpectedCalls = nil
}
func (suite *MetaReloadSuite) TestReloadFromKV() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
suite.Run("ListSegments_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(&rootcoordpb.ShowCollectionIDsResponse{
Status: merr.Success(),
DbCollections: []*rootcoordpb.DBCollections{
{
DbName: "db_1",
CollectionIDs: []int64{100},
},
},
}, nil)
suite.catalog.EXPECT().ListSegments(mock.Anything, mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("ListChannelCheckpoint_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSegments(mock.Anything, mock.Anything).Return([]*datapb.SegmentInfo{}, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("ok", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(&rootcoordpb.ShowCollectionIDsResponse{
Status: merr.Success(),
DbCollections: []*rootcoordpb.DBCollections{
{
DbName: "db_1",
CollectionIDs: []int64{1},
},
},
}, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSegments(mock.Anything, mock.Anything).Return([]*datapb.SegmentInfo{
{
ID: 1,
CollectionID: 1,
PartitionID: 1,
State: commonpb.SegmentState_Flushed,
},
}, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(map[string]*msgpb.MsgPosition{
"ch": {
ChannelName: "cn",
MsgID: []byte{},
Timestamp: 1000,
},
}, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.NoError(err)
suite.MetricsEqual(metrics.DataCoordNumSegments.WithLabelValues(metrics.FlushedSegmentLabel, datapb.SegmentLevel_Legacy.String(), "unsorted", "0", "legacy"), 1)
})
suite.Run("ListIndexes_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("ListAnalyzeTasks_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("ListPartitionStatsInfos_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("ListCompactionTask_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("ListStatsTasks_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("ListSnapshots_fail", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, errors.New("mock"))
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
_, err := newMeta(ctx, suite.catalog, nil, brk)
suite.Error(err)
})
suite.Run("test list segments", func() {
defer suite.resetMock()
brk := broker.NewMockBroker(suite.T())
brk.EXPECT().ShowCollectionIDs(mock.Anything).Return(&rootcoordpb.ShowCollectionIDsResponse{
Status: merr.Success(),
DbCollections: []*rootcoordpb.DBCollections{
{
DbName: "db_1",
CollectionIDs: []int64{100, 101, 102},
},
{
DbName: "db_2",
CollectionIDs: []int64{200, 201, 202},
},
},
}, nil)
suite.catalog.EXPECT().ListIndexes(mock.Anything).Return([]*model.Index{}, nil)
suite.catalog.EXPECT().ListSegmentIndexes(mock.Anything, mock.Anything).Return([]*model.SegmentIndex{}, nil).Maybe()
suite.catalog.EXPECT().ListAnalyzeTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListCompactionTask(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListPartitionStatsInfos(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListStatsTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListChannelCheckpoint(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSnapshots(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshJobs(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListExternalCollectionRefreshTasks(mock.Anything).Return(nil, nil)
suite.catalog.EXPECT().ListSegments(mock.Anything, mock.Anything).RunAndReturn(
func(ctx context.Context, collectionID int64) ([]*datapb.SegmentInfo, error) {
return []*datapb.SegmentInfo{
{
ID: rand.Int63(),
CollectionID: collectionID,
State: commonpb.SegmentState_Flushed,
},
}, nil
})
meta, err := newMeta(ctx, suite.catalog, nil, brk)
suite.NoError(err)
for _, collectionID := range []int64{100, 101, 102, 200, 201, 202} {
segments := meta.GetSegmentsOfCollection(ctx, collectionID)
suite.Len(segments, 1)
suite.Equal(collectionID, segments[0].GetCollectionID())
}
})
}
type MetaBasicSuite struct {
testutils.PromMetricsSuite
collID int64
partIDs []int64
channelName string
meta *meta
}
func (suite *MetaBasicSuite) SetupSuite() {
paramtable.Init()
}
func (suite *MetaBasicSuite) SetupTest() {
suite.collID = 1
suite.partIDs = []int64{100, 101}
suite.channelName = "c1"
meta, err := newMemoryMeta(suite.T())
suite.Require().NoError(err)
suite.meta = meta
}
func (suite *MetaBasicSuite) getCollectionInfo(partIDs ...int64) *collectionInfo {
testSchema := newTestSchema()
return &collectionInfo{
ID: suite.collID,
Schema: testSchema,
Partitions: partIDs,
StartPositions: []*commonpb.KeyDataPair{},
}
}
func (suite *MetaBasicSuite) TestCollection() {
meta := suite.meta
info := suite.getCollectionInfo(suite.partIDs...)
meta.AddCollection(info)
collInfo := meta.GetCollection(suite.collID)
suite.Require().NotNil(collInfo)
// check partition info
suite.EqualValues(suite.collID, collInfo.ID)
suite.EqualValues(info.Schema, collInfo.Schema)
suite.EqualValues(len(suite.partIDs), len(collInfo.Partitions))
suite.ElementsMatch(info.Partitions, collInfo.Partitions)
suite.MetricsEqual(metrics.DataCoordNumCollections.WithLabelValues(), 1)
}
func (suite *MetaBasicSuite) TestCompleteCompactionMutation() {
getLatestSegments := func() *SegmentsInfo {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000, 10001)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000, 20001)},
// latest segment has 2 deltalogs, one submit for compaction, one is appended before compaction done
Deltalogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 30000), getFieldBinlogIDs(0, 30001)},
NumOfRows: 2,
}},
2: {SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 11000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 21000)},
// latest segment has 2 deltalogs, one submit for compaction, one is appended before compaction done
Deltalogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 31000), getFieldBinlogIDs(0, 31001)},
NumOfRows: 2,
}},
} {
latestSegments.SetSegment(segID, segment)
}
return latestSegments
}
mockChMgr := mocks.NewChunkManager(suite.T())
suite.Run("test complete with empty result segments", func() {
// Test case: when all data is deleted, compaction result should have empty Segments slice
// This verifies that completeMixCompactionMutation correctly handles the case
// where no output segments are produced (all data deleted during compaction)
latestSegments := getLatestSegments()
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{}, // Empty - all data deleted
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, mutation, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Empty(infos) // No output segments when all data deleted
suite.NotNil(mutation)
// check compactFrom segments are marked as Dropped
for _, segID := range []int64{1, 2} {
seg := m.GetSegment(context.TODO(), segID)
suite.Equal(commonpb.SegmentState_Dropped, seg.GetState())
suite.NotEmpty(seg.GetDroppedAt())
suite.True(seg.GetCompacted())
}
// check mutation metrics - only input segments changed to Dropped
suite.EqualValues(-4, mutation.rowCountChange)
flushedUnsorted := mutation.stateChange[datapb.SegmentLevel_L1.String()][commonpb.SegmentState_Flushed.String()][getSortStatus(false)]["0"][segmentMetricFormatLegacy]
suite.EqualValues(-2, flushedUnsorted)
droppedUnsorted := mutation.stateChange[datapb.SegmentLevel_L1.String()][commonpb.SegmentState_Dropped.String()][getSortStatus(false)]["0"][segmentMetricFormatLegacy]
suite.EqualValues(2, droppedUnsorted)
})
suite.Run("test complete with compactTo 0 num of rows", func() {
latestSegments := getLatestSegments()
compactToSeg := &datapb.CompactionSegment{
SegmentID: 4,
InsertLogs: []*datapb.FieldBinlog{},
Field2StatslogPaths: []*datapb.FieldBinlog{},
NumOfRows: 0,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{compactToSeg},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, mutation, err := m.CompleteCompactionMutation(context.TODO(), task, result)
assert.NoError(suite.T(), err)
suite.Equal(1, len(infos))
info := infos[0]
suite.NoError(err)
suite.NotNil(info)
suite.NotNil(mutation)
// check compact to segments
suite.EqualValues(4, info.GetID())
suite.Equal(datapb.SegmentLevel_L1, info.GetLevel())
suite.Equal(commonpb.SegmentState_Dropped, info.GetState())
suite.Empty(info.GetBinlogs())
suite.Empty(info.GetStatslogs())
// check compactFrom segments
for _, segID := range []int64{1, 2} {
seg := m.GetSegment(context.TODO(), segID)
suite.Equal(commonpb.SegmentState_Dropped, seg.GetState())
suite.NotEmpty(seg.GetDroppedAt())
suite.EqualValues(segID, seg.GetID())
suite.ElementsMatch(latestSegments.segments[segID].GetBinlogs(), seg.GetBinlogs())
suite.ElementsMatch(latestSegments.segments[segID].GetStatslogs(), seg.GetStatslogs())
suite.ElementsMatch(latestSegments.segments[segID].GetDeltalogs(), seg.GetDeltalogs())
}
// check mutation metrics
suite.EqualValues(2, len(mutation.stateChange[datapb.SegmentLevel_L1.String()]))
suite.EqualValues(-4, mutation.rowCountChange)
suite.EqualValues(0, mutation.rowCountAccChange)
flushedUnsorted := mutation.stateChange[datapb.SegmentLevel_L1.String()][commonpb.SegmentState_Flushed.String()][getSortStatus(false)]["0"][segmentMetricFormatLegacy]
suite.EqualValues(-2, flushedUnsorted)
droppedUnsorted := mutation.stateChange[datapb.SegmentLevel_L1.String()][commonpb.SegmentState_Dropped.String()][getSortStatus(false)]["0"][segmentMetricFormatLegacy]
suite.EqualValues(3, droppedUnsorted)
})
suite.Run("test complete compaction mutation", func() {
latestSegments := getLatestSegments()
compactToSeg := &datapb.CompactionSegment{
SegmentID: 3,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 2,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{compactToSeg},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, mutation, err := m.CompleteCompactionMutation(context.TODO(), task, result)
assert.NoError(suite.T(), err)
suite.Equal(1, len(infos))
info := infos[0]
suite.NoError(err)
suite.NotNil(info)
suite.NotNil(mutation)
// check newSegment
suite.EqualValues(3, info.GetID())
suite.Equal(datapb.SegmentLevel_L1, info.GetLevel())
suite.Equal(commonpb.SegmentState_Flushed, info.GetState())
binlogs := info.GetBinlogs()
for _, fbinlog := range binlogs {
for _, blog := range fbinlog.GetBinlogs() {
suite.Empty(blog.GetLogPath())
suite.EqualValues(50000, blog.GetLogID())
}
}
statslogs := info.GetStatslogs()
for _, fbinlog := range statslogs {
for _, blog := range fbinlog.GetBinlogs() {
suite.Empty(blog.GetLogPath())
suite.EqualValues(50001, blog.GetLogID())
}
}
// check compactFrom segments
for _, segID := range []int64{1, 2} {
seg := m.GetSegment(context.TODO(), segID)
suite.Equal(commonpb.SegmentState_Dropped, seg.GetState())
suite.NotEmpty(seg.GetDroppedAt())
suite.EqualValues(segID, seg.GetID())
suite.ElementsMatch(latestSegments.segments[segID].GetBinlogs(), seg.GetBinlogs())
suite.ElementsMatch(latestSegments.segments[segID].GetStatslogs(), seg.GetStatslogs())
suite.ElementsMatch(latestSegments.segments[segID].GetDeltalogs(), seg.GetDeltalogs())
}
// check mutation metrics
suite.EqualValues(2, len(mutation.stateChange[datapb.SegmentLevel_L1.String()]))
suite.EqualValues(-2, mutation.rowCountChange)
suite.EqualValues(2, mutation.rowCountAccChange)
flushedCount := mutation.stateChange[datapb.SegmentLevel_L1.String()][commonpb.SegmentState_Flushed.String()][getSortStatus(false)]["0"][segmentMetricFormatLegacy]
suite.EqualValues(-1, flushedCount)
droppedCount := mutation.stateChange[datapb.SegmentLevel_L1.String()][commonpb.SegmentState_Dropped.String()][getSortStatus(false)]["0"][segmentMetricFormatLegacy]
suite.EqualValues(2, droppedCount)
})
suite.Run("mixed schema version mix compaction uses task schema version", func() {
latestSegments := getLatestSegments()
latestSegments.segments[1].SchemaVersion = 2
latestSegments.segments[2].SchemaVersion = 3
compactToSeg := &datapb.CompactionSegment{
SegmentID: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 2,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{compactToSeg},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Schema: &schemapb.CollectionSchema{Version: 9},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Len(infos, 1)
suite.EqualValues(9, infos[0].GetSchemaVersion())
})
suite.Run("mixed schema version clustering compaction uses task schema version", func() {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 2,
SchemaVersion: 1,
}},
2: {SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 11000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 21000)},
NumOfRows: 2,
SchemaVersion: 4,
}},
} {
latestSegments.SetSegment(segID, segment)
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{
SegmentID: 3,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 4,
}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_ClusteringCompaction,
Schema: &schemapb.CollectionSchema{Version: 9},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Len(infos, 1)
suite.EqualValues(9, infos[0].GetSchemaVersion())
})
suite.Run("test mix complete rejects nil task schema", func() {
latestSegments := getLatestSegments()
compactToSeg := &datapb.CompactionSegment{
SegmentID: 7,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 2,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{compactToSeg},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, mutation, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.Error(err)
suite.True(errors.Is(err, merr.ErrIllegalCompactionPlan))
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("test L2 sort", func() {
getLatestSegments := func() *SegmentsInfo {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L2,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000, 10001)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000, 20001)},
// latest segment has 2 deltalogs, one submit for compaction, one is appended before compaction done
Deltalogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 30000), getFieldBinlogIDs(0, 30001)},
NumOfRows: 2,
StorageVersion: storage.StorageV1,
SchemaVersion: 2,
}},
} {
latestSegments.SetSegment(segID, segment)
}
return latestSegments
}
latestSegments := getLatestSegments()
compactToSeg := &datapb.CompactionSegment{
SegmentID: 2,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 2,
StorageVersion: storage.StorageV2,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{compactToSeg},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1},
Type: datapb.CompactionType_SortCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, mutation, err := m.CompleteCompactionMutation(context.TODO(), task, result)
assert.NoError(suite.T(), err)
suite.Equal(1, len(infos))
info := infos[0]
suite.NoError(err)
suite.NotNil(info)
suite.NotNil(mutation)
// check newSegment
suite.EqualValues(2, info.GetID())
suite.Equal(datapb.SegmentLevel_L2, info.GetLevel())
suite.Equal(commonpb.SegmentState_Flushed, info.GetState())
suite.Equal(storage.StorageV2, info.GetStorageVersion())
suite.EqualValues(3, info.GetSchemaVersion())
binlogs := info.GetBinlogs()
for _, fbinlog := range binlogs {
for _, blog := range fbinlog.GetBinlogs() {
suite.Empty(blog.GetLogPath())
suite.EqualValues(50000, blog.GetLogID())
}
}
statslogs := info.GetStatslogs()
for _, fbinlog := range statslogs {
for _, blog := range fbinlog.GetBinlogs() {
suite.Empty(blog.GetLogPath())
suite.EqualValues(50001, blog.GetLogID())
}
}
// check compactFrom segments
for _, segID := range []int64{1} {
seg := m.GetSegment(context.TODO(), segID)
suite.Equal(commonpb.SegmentState_Dropped, seg.GetState())
suite.NotEmpty(seg.GetDroppedAt())
}
latestSegments = getLatestSegments()
compactToSeg = &datapb.CompactionSegment{
SegmentID: 3,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 2,
StorageVersion: storage.StorageV2,
}
result = &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{compactToSeg},
}
task = &datapb.CompactionTask{
InputSegments: []UniqueID{1},
Type: datapb.CompactionType_SortCompaction,
}
m = &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, mutation, err = m.CompleteCompactionMutation(context.TODO(), task, result)
suite.Error(err)
suite.True(errors.Is(err, merr.ErrIllegalCompactionPlan))
suite.Nil(infos)
suite.Nil(mutation)
suite.Equal(commonpb.SegmentState_Flushed, m.GetSegment(context.TODO(), 1).GetState())
})
suite.Run("mix compaction normalizes commit_timestamp to zero", func() {
// Input: two import segments with different commit_timestamps.
// After compaction, row timestamps are already rewritten to commit_ts
// by the compactor, so the output segment is normalized (CommitTimestamp = 0).
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 2, CommitTimestamp: 5000,
}})
latestSegments.SetSegment(2, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 2, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 3, CommitTimestamp: 8000,
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{SegmentID: 10, NumOfRows: 5}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.EqualValues(0, infos[0].GetCommitTimestamp(), "compaction normalizes commit_timestamp: row timestamps already rewritten")
})
suite.Run("sort compaction normalizes commit_timestamp to zero", func() {
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L2,
NumOfRows: 2, CommitTimestamp: 7777,
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{SegmentID: 2, NumOfRows: 2}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1},
Type: datapb.CompactionType_SortCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.EqualValues(0, infos[0].GetCommitTimestamp(), "sort compaction normalizes commit_timestamp: row timestamps already rewritten")
})
suite.Run("clustering compaction normalizes commit_timestamp to zero", func() {
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 2, CommitTimestamp: 6000,
}})
latestSegments.SetSegment(2, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 2, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 3, CommitTimestamp: 4000,
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{SegmentID: 10, NumOfRows: 5}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_ClusteringCompaction,
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.EqualValues(0, infos[0].GetCommitTimestamp(), "clustering compaction normalizes commit_timestamp: row timestamps already rewritten")
})
suite.Run("mix compaction with mixed import and normal segments normalizes to zero", func() {
// One import segment (commitTs=5000) + one normal segment (commitTs=0).
// After compaction, row timestamps are rewritten, so output is normalized.
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 2, CommitTimestamp: 5000,
}})
latestSegments.SetSegment(2, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 2, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 3, CommitTimestamp: 0,
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{SegmentID: 10, NumOfRows: 5}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.EqualValues(0, infos[0].GetCommitTimestamp(), "compaction normalizes commit_timestamp: row timestamps already rewritten")
})
suite.Run("mix compaction with no import segments sets commit_timestamp to 0", func() {
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 2, CommitTimestamp: 0,
}})
latestSegments.SetSegment(2, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 2, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 3, CommitTimestamp: 0,
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{SegmentID: 10, NumOfRows: 5}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.EqualValues(0, infos[0].GetCommitTimestamp(), "normal segment compaction must not set commit_timestamp")
})
suite.Run("sort compaction normalizes stale import fallback start position", func() {
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L2,
NumOfRows: 2, CommitTimestamp: 5000,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 1000},
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{SegmentID: 2, NumOfRows: 2}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1},
Type: datapb.CompactionType_SortCompaction,
Channel: "ch-1",
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.EqualValues(5000, infos[0].GetStartPosition().GetTimestamp())
suite.EqualValues(0, infos[0].GetCommitTimestamp(), "sort compaction normalizes commit_timestamp after rewriting row timestamps")
})
suite.Run("mix compaction rejects stale import fallback start position", func() {
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 2, CommitTimestamp: 5000,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 1000},
}})
latestSegments.SetSegment(2, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 2, CollectionID: 100, PartitionID: 10,
State: commonpb.SegmentState_Flushed, Level: datapb.SegmentLevel_L1,
NumOfRows: 3, CommitTimestamp: 0,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 2000},
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{SegmentID: 10, NumOfRows: 5}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Channel: "ch-1",
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.Error(err)
suite.Contains(err.Error(), "earlier than max input commit timestamp")
suite.Nil(infos)
})
}
func (suite *MetaBasicSuite) TestValidateSegmentState_BlockedBySnapshot() {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
}},
} {
latestSegments.SetSegment(segID, segment)
}
task := &datapb.CompactionTask{
PlanID: 999,
InputSegments: []UniqueID{1},
CollectionID: 100,
Type: datapb.CompactionType_MixCompaction,
}
suite.Run("rejected by snapshot pending collection", func() {
sm := createTestSnapshotMetaLoaded(suite.T())
sm.SetSnapshotPending(100)
m := &meta{
segments: latestSegments,
snapshotMeta: sm,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(task)
suite.Error(err)
suite.Contains(err.Error(), "compaction blocked")
})
suite.Run("rejected by segment protection", func() {
sm := createTestSnapshotMetaLoaded(suite.T())
futureTs := uint64(time.Now().Unix()) + 3600
sm.segmentProtectionMu.Lock()
sm.segmentProtectionUntil[1] = futureTs
sm.segmentProtectionMu.Unlock()
m := &meta{
segments: latestSegments,
snapshotMeta: sm,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(task)
suite.Error(err)
suite.Contains(err.Error(), "segment 1")
})
suite.Run("passes when no snapshot", func() {
sm := createTestSnapshotMetaLoaded(suite.T())
m := &meta{
segments: latestSegments,
snapshotMeta: sm,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(task)
suite.NoError(err)
})
suite.Run("passes when snapshotMeta is nil", func() {
m := &meta{
segments: latestSegments,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(task)
suite.NoError(err)
})
suite.Run("rejected when only middle segment is protected in multi-segment task", func() {
multiSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{ID: 1, CollectionID: 100, PartitionID: 10, State: commonpb.SegmentState_Flushed}},
2: {SegmentInfo: &datapb.SegmentInfo{ID: 2, CollectionID: 100, PartitionID: 10, State: commonpb.SegmentState_Flushed}},
3: {SegmentInfo: &datapb.SegmentInfo{ID: 3, CollectionID: 100, PartitionID: 10, State: commonpb.SegmentState_Flushed}},
} {
multiSegments.SetSegment(segID, segment)
}
multiTask := &datapb.CompactionTask{
PlanID: 998,
InputSegments: []UniqueID{1, 2, 3},
CollectionID: 100,
Type: datapb.CompactionType_MixCompaction,
}
sm := createTestSnapshotMetaLoaded(suite.T())
futureTs := uint64(time.Now().Unix()) + 3600
// Only protect segment 2
sm.segmentProtectionMu.Lock()
sm.segmentProtectionUntil[2] = futureTs
sm.segmentProtectionMu.Unlock()
m := &meta{
segments: multiSegments,
snapshotMeta: sm,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(multiTask)
suite.Error(err)
suite.Contains(err.Error(), "segment 2")
})
suite.Run("passes when protection expired", func() {
sm := createTestSnapshotMetaLoaded(suite.T())
pastTs := uint64(time.Now().Unix()) - 100
sm.segmentProtectionMu.Lock()
sm.segmentProtectionUntil[1] = pastTs
sm.segmentProtectionMu.Unlock()
m := &meta{
segments: latestSegments,
snapshotMeta: sm,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(task)
suite.NoError(err)
})
// Regression: L0 delete compaction must bypass snapshot protection checks entirely.
// Snapshots only reference sealed L1/L2 segments; blocking L0 here would cause
// delta log accumulation, query latency spikes, and write stalls.
suite.Run("L0 compaction passes even when collection is snapshot-pending", func() {
sm := createTestSnapshotMetaLoaded(suite.T())
sm.SetSnapshotPending(100)
m := &meta{
segments: latestSegments,
snapshotMeta: sm,
}
l0Task := &datapb.CompactionTask{
PlanID: 8001,
InputSegments: []UniqueID{1},
CollectionID: 100,
Type: datapb.CompactionType_Level0DeleteCompaction,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(l0Task)
suite.NoError(err)
})
suite.Run("L0 compaction passes even when input segment is snapshot-protected", func() {
sm := createTestSnapshotMetaLoaded(suite.T())
futureTs := uint64(time.Now().Unix()) + 3600
sm.segmentProtectionMu.Lock()
sm.segmentProtectionUntil[1] = futureTs
sm.segmentProtectionMu.Unlock()
m := &meta{
segments: latestSegments,
snapshotMeta: sm,
}
l0Task := &datapb.CompactionTask{
PlanID: 8002,
InputSegments: []UniqueID{1},
CollectionID: 100,
Type: datapb.CompactionType_Level0DeleteCompaction,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(l0Task)
suite.NoError(err)
})
// Regression: collection-level block should produce ErrCompactionBlocked,
// not ErrServiceInternal, so SRE alerting does not treat it as a P0 fault.
suite.Run("rejection uses ErrCompactionBlocked, not ErrServiceInternal", func() {
sm := createTestSnapshotMetaLoaded(suite.T())
sm.SetSnapshotPending(100)
m := &meta{
segments: latestSegments,
snapshotMeta: sm,
}
err := m.ValidateSegmentStateBeforeCompleteCompactionMutation(task)
suite.Error(err)
suite.True(errors.Is(err, merr.ErrCompactionBlocked))
suite.False(errors.Is(err, merr.ErrServiceInternal))
})
}
func (suite *MetaBasicSuite) TestGetMaxPosition() {
suite.Run("nil_positions", func() {
pos := getMaxPosition(nil)
suite.Nil(pos)
})
suite.Run("single_position", func() {
p := &msgpb.MsgPosition{Timestamp: 100}
pos := getMaxPosition([]*msgpb.MsgPosition{p})
suite.Equal(uint64(100), pos.GetTimestamp())
})
suite.Run("multiple_positions", func() {
pos := getMaxPosition([]*msgpb.MsgPosition{
{Timestamp: 100},
{Timestamp: 300},
{Timestamp: 200},
})
suite.Equal(uint64(300), pos.GetTimestamp())
})
suite.Run("with_nil_entries", func() {
pos := getMaxPosition([]*msgpb.MsgPosition{
nil,
{Timestamp: 50},
nil,
{Timestamp: 200},
})
suite.Equal(uint64(200), pos.GetTimestamp())
})
}
func (suite *MetaBasicSuite) TestRecalculateSegmentPosition() {
channel := "ch-1"
fallbackStart := &msgpb.MsgPosition{ChannelName: channel, Timestamp: 10}
fallbackDml := &msgpb.MsgPosition{ChannelName: channel, Timestamp: 90}
suite.Run("recalculates_from_binlog_timestamps", func() {
binlogs := []*datapb.FieldBinlog{
{
FieldID: 0,
Binlogs: []*datapb.Binlog{
{LogID: 1, TimestampFrom: 100, TimestampTo: 200},
{LogID: 2, TimestampFrom: 150, TimestampTo: 300},
},
},
{
FieldID: 1,
Binlogs: []*datapb.Binlog{
{LogID: 3, TimestampFrom: 80, TimestampTo: 250},
},
},
}
startPos, dmlPos := recalculateSegmentPosition(binlogs, channel, fallbackStart, fallbackDml)
suite.Equal(uint64(80), startPos.GetTimestamp())
suite.Equal(uint64(300), dmlPos.GetTimestamp())
suite.Equal(channel, startPos.GetChannelName())
suite.Equal(channel, dmlPos.GetChannelName())
})
suite.Run("fallback_when_timestamps_zero", func() {
binlogs := []*datapb.FieldBinlog{
{
FieldID: 0,
Binlogs: []*datapb.Binlog{
{LogID: 1, TimestampFrom: 0, TimestampTo: 0},
},
},
}
startPos, dmlPos := recalculateSegmentPosition(binlogs, channel, fallbackStart, fallbackDml)
suite.Equal(fallbackStart, startPos)
suite.Equal(fallbackDml, dmlPos)
})
suite.Run("fallback_when_no_binlogs", func() {
startPos, dmlPos := recalculateSegmentPosition(nil, channel, fallbackStart, fallbackDml)
suite.Equal(fallbackStart, startPos)
suite.Equal(fallbackDml, dmlPos)
})
suite.Run("fallback_when_empty_binlogs", func() {
startPos, dmlPos := recalculateSegmentPosition([]*datapb.FieldBinlog{}, channel, fallbackStart, fallbackDml)
suite.Equal(fallbackStart, startPos)
suite.Equal(fallbackDml, dmlPos)
})
}
func (suite *MetaBasicSuite) TestCompleteCompactionMutation_RecalculatePositions() {
mockChMgr := mocks.NewChunkManager(suite.T())
// Helper to build FieldBinlog with timestamps
fieldBinlogWithTimestamps := func(fieldID int64, logID int64, tsFrom, tsTo uint64) *datapb.FieldBinlog {
return &datapb.FieldBinlog{
FieldID: fieldID,
Binlogs: []*datapb.Binlog{
{LogID: logID, TimestampFrom: tsFrom, TimestampTo: tsTo},
},
}
}
suite.Run("mix_compaction_recalculates_positions_from_binlogs", func() {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 999},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 1},
}},
2: {SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 11000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 21000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 888},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 2},
}},
} {
latestSegments.SetSegment(segID, segment)
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{
SegmentID: 3,
InsertLogs: []*datapb.FieldBinlog{fieldBinlogWithTimestamps(0, 50000, 100, 500)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 4,
}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Channel: "ch-1",
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
// Should use binlog timestamps, NOT inherited positions
suite.Equal(uint64(100), infos[0].GetStartPosition().GetTimestamp())
suite.Equal(uint64(500), infos[0].GetDmlPosition().GetTimestamp())
suite.Equal("ch-1", infos[0].GetStartPosition().GetChannelName())
})
suite.Run("mix_compaction_fallback_when_no_timestamps", func() {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 50},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 200},
}},
2: {SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 11000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 21000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 100},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 300},
}},
} {
latestSegments.SetSegment(segID, segment)
}
// Result binlogs have no timestamps (legacy)
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{
SegmentID: 3,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 4,
}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_MixCompaction,
Channel: "ch-1",
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
// Fallback: StartPosition = min, DmlPosition = max of source segments
suite.Equal(uint64(50), infos[0].GetStartPosition().GetTimestamp())
suite.Equal(uint64(300), infos[0].GetDmlPosition().GetTimestamp())
})
suite.Run("cluster_compaction_recalculates_positions", func() {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 999},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 1},
}},
2: {SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 11000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 21000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 888},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 2},
}},
} {
latestSegments.SetSegment(segID, segment)
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{
SegmentID: 3,
InsertLogs: []*datapb.FieldBinlog{fieldBinlogWithTimestamps(0, 50000, 150, 600)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 4,
}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_ClusteringCompaction,
Channel: "ch-1",
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.Equal(uint64(150), infos[0].GetStartPosition().GetTimestamp())
suite.Equal(uint64(600), infos[0].GetDmlPosition().GetTimestamp())
suite.Equal(datapb.SegmentLevel_L2, infos[0].GetLevel())
})
suite.Run("cluster_compaction_fallback_when_no_timestamps", func() {
latestSegments := NewSegmentsInfo()
for segID, segment := range map[UniqueID]*SegmentInfo{
1: {SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 50},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 400},
}},
2: {SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 11000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 21000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 80},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 500},
}},
} {
latestSegments.SetSegment(segID, segment)
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{
SegmentID: 3,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 4,
}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1, 2},
Type: datapb.CompactionType_ClusteringCompaction,
Channel: "ch-1",
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.Equal(uint64(50), infos[0].GetStartPosition().GetTimestamp())
suite.Equal(uint64(500), infos[0].GetDmlPosition().GetTimestamp())
})
suite.Run("sort_compaction_recalculates_positions", func() {
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L2,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 999},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 1},
InsertChannel: "ch-1",
StorageVersion: storage.StorageV1,
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{
SegmentID: 2,
InsertLogs: []*datapb.FieldBinlog{fieldBinlogWithTimestamps(0, 50000, 200, 800)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 2,
StorageVersion: storage.StorageV2,
}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1},
Type: datapb.CompactionType_SortCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.Equal(uint64(200), infos[0].GetStartPosition().GetTimestamp())
suite.Equal(uint64(800), infos[0].GetDmlPosition().GetTimestamp())
})
suite.Run("sort_compaction_fallback_when_no_timestamps", func() {
latestSegments := NewSegmentsInfo()
latestSegments.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L2,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 2,
StartPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 77},
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch-1", Timestamp: 333},
InsertChannel: "ch-1",
StorageVersion: storage.StorageV1,
}})
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{{
SegmentID: 2,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50000)},
Field2StatslogPaths: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 50001)},
NumOfRows: 2,
StorageVersion: storage.StorageV2,
}},
}
task := &datapb.CompactionTask{
InputSegments: []UniqueID{1},
Type: datapb.CompactionType_SortCompaction,
Schema: &schemapb.CollectionSchema{Version: 1},
}
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: latestSegments,
chunkManager: mockChMgr,
}
infos, _, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.Require().Equal(1, len(infos))
suite.Equal(uint64(77), infos[0].GetStartPosition().GetTimestamp())
suite.Equal(uint64(333), infos[0].GetDmlPosition().GetTimestamp())
})
}
func (suite *MetaBasicSuite) TestSetSegment() {
meta := suite.meta
catalog := mocks2.NewDataCoordCatalog(suite.T())
meta.catalog = catalog
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
suite.Run("normal", func() {
segmentID := int64(1000)
catalog.EXPECT().AddSegment(mock.Anything, mock.Anything).Return(nil).Once()
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: segmentID,
MaxRowNum: 30000,
CollectionID: suite.collID,
InsertChannel: suite.channelName,
State: commonpb.SegmentState_Flushed,
})
err := meta.AddSegment(ctx, segment)
suite.Require().NoError(err)
noOp := func(segment *SegmentInfo) bool {
return true
}
catalog.EXPECT().AlterSegments(mock.Anything, mock.Anything).Return(nil).Once()
err = meta.UpdateSegment(segmentID, noOp)
suite.NoError(err)
})
suite.Run("not_updated", func() {
segmentID := int64(1001)
catalog.EXPECT().AddSegment(mock.Anything, mock.Anything).Return(nil).Once()
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: segmentID,
MaxRowNum: 30000,
CollectionID: suite.collID,
InsertChannel: suite.channelName,
State: commonpb.SegmentState_Flushed,
})
err := meta.AddSegment(ctx, segment)
suite.Require().NoError(err)
noOp := func(segment *SegmentInfo) bool {
return false
}
err = meta.UpdateSegment(segmentID, noOp)
suite.NoError(err)
})
suite.Run("catalog_error", func() {
segmentID := int64(1002)
catalog.EXPECT().AddSegment(mock.Anything, mock.Anything).Return(nil).Once()
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: segmentID,
MaxRowNum: 30000,
CollectionID: suite.collID,
InsertChannel: suite.channelName,
State: commonpb.SegmentState_Flushed,
})
err := meta.AddSegment(ctx, segment)
suite.Require().NoError(err)
noOp := func(segment *SegmentInfo) bool {
return true
}
catalog.EXPECT().AlterSegments(mock.Anything, mock.Anything).Return(errors.New("mocked")).Once()
err = meta.UpdateSegment(segmentID, noOp)
suite.Error(err)
})
suite.Run("segment_not_found", func() {
segmentID := int64(1003)
noOp := func(segment *SegmentInfo) bool {
return true
}
err := meta.UpdateSegment(segmentID, noOp)
suite.Error(err)
suite.ErrorIs(err, merr.ErrSegmentNotFound)
})
}
func (suite *MetaBasicSuite) TestCompleteBumpSchemaVersionCompactionMutation() {
// Helper to build a SegmentsInfo containing a single healthy Flushed segment with the given ID.
makeSegments := func(segID int64, state commonpb.SegmentState) *SegmentsInfo {
segs := NewSegmentsInfo()
segs.SetSegment(segID, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: segID,
CollectionID: 100,
PartitionID: 10,
State: state,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 20000)},
NumOfRows: 5,
SchemaVersion: 1,
}})
return segs
}
suite.Run("too many input segments", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1, 2}, // two inputs — should error
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{SegmentID: 1},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("too many result segments", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{SegmentID: 1},
{SegmentID: 2}, // two results — should error
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("segment not found", func() {
// Segment 99 is not in meta.
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: NewSegmentsInfo(),
}
task := &datapb.CompactionTask{
InputSegments: []int64{99},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{SegmentID: 99},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.ErrorIs(err, merr.ErrSegmentNotFound)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("segment dropped", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Dropped),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{SegmentID: 1},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.ErrorIs(err, merr.ErrSegmentNotFound)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("replacement result without preallocated ID rejected", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 999,
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("replacement result drops old segment and creates flushed new segment", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{
Version: 3,
},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
oldBefore := m.segments.GetSegment(1)
oldBefore.InsertChannel = "test-channel"
oldBefore.StartPosition = &msgpb.MsgPosition{ChannelName: "test-channel", Timestamp: 10}
oldBefore.DmlPosition = &msgpb.MsgPosition{ChannelName: "test-channel", Timestamp: 20}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{
{
FieldID: 0,
Binlogs: []*datapb.Binlog{
{LogID: 10001, TimestampFrom: 100, TimestampTo: 300},
{LogID: 10002, TimestampFrom: 80, TimestampTo: 250},
},
},
},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.EqualValues(2, infos[0].GetID())
suite.Equal(commonpb.SegmentState_Flushed, infos[0].GetState())
suite.EqualValues(3, infos[0].GetSchemaVersion())
suite.Equal([]int64{int64(1)}, infos[0].GetCompactionFrom())
suite.True(infos[0].GetCreatedByCompaction())
old := m.segments.GetSegment(1)
suite.Require().NotNil(old)
suite.Equal(commonpb.SegmentState_Dropped, old.GetState())
suite.True(old.GetCompacted())
suite.NotZero(old.GetDroppedAt())
created := m.segments.GetSegment(2)
suite.Require().NotNil(created)
suite.Equal(commonpb.SegmentState_Flushed, created.GetState())
suite.EqualValues(3, created.GetSchemaVersion())
suite.Equal("manifest-new", created.GetManifestPath())
suite.False(created.GetIsInvisible())
suite.Equal("test-channel", created.GetStartPosition().GetChannelName())
suite.EqualValues(80, created.GetStartPosition().GetTimestamp())
suite.Equal("test-channel", created.GetDmlPosition().GetChannelName())
suite.EqualValues(300, created.GetDmlPosition().GetTimestamp())
})
suite.Run("zero-row replacement drops old segment and creates dropped new segment without manifest", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 0,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.EqualValues(2, infos[0].GetID())
suite.Equal(commonpb.SegmentState_Dropped, infos[0].GetState())
suite.EqualValues(0, infos[0].GetNumOfRows())
suite.Empty(infos[0].GetManifestPath())
old := m.segments.GetSegment(1)
suite.Require().NotNil(old)
suite.Equal(commonpb.SegmentState_Dropped, old.GetState())
suite.True(old.GetCompacted())
created := m.segments.GetSegment(2)
suite.Require().NotNil(created)
suite.Equal(commonpb.SegmentState_Dropped, created.GetState())
suite.Empty(created.GetManifestPath())
})
suite.Run("replacement preserves input sort flags", func() {
for _, tc := range []struct {
name string
inputIsSorted bool
inputIsSortedByNamespace bool
resultIsSorted bool
resultIsSortedByNamespace bool
}{
{name: "sorted by pk", inputIsSorted: true},
{name: "sorted by namespace", inputIsSortedByNamespace: true},
{name: "ignores result-only flags", resultIsSorted: true, resultIsSortedByNamespace: true},
} {
suite.Run(tc.name, func() {
segs := makeSegments(1, commonpb.SegmentState_Flushed)
oldSegment := segs.GetSegment(1)
oldSegment.IsSorted = tc.inputIsSorted
oldSegment.IsSortedByNamespace = tc.inputIsSortedByNamespace
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
IsSorted: tc.resultIsSorted,
IsSortedByNamespace: tc.resultIsSortedByNamespace,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.Equal(tc.inputIsSorted, infos[0].GetIsSorted())
suite.Equal(tc.inputIsSortedByNamespace, infos[0].GetIsSortedByNamespace())
created := m.segments.GetSegment(2)
suite.Require().NotNil(created)
suite.Equal(tc.inputIsSorted, created.GetIsSorted())
suite.Equal(tc.inputIsSortedByNamespace, created.GetIsSortedByNamespace())
})
}
})
suite.Run("replacement result accepts same task schema", func() {
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segs.GetSegment(1).SchemaVersion = 3
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.EqualValues(2, infos[0].GetID())
suite.EqualValues(3, infos[0].GetSchemaVersion())
suite.False(infos[0].GetIsInvisible())
})
suite.Run("replacement result rejects stale task schema", func() {
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segs.GetSegment(1).SchemaVersion = 3
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 2},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("replacement result rejects invisible input", func() {
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segs.GetSegment(1).IsInvisible = true
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("replacement result rejects nil task schema", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("replacement result rejects non-begin preallocated segment ID", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 4},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 3,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("missing manifest rejected", func() {
segs := NewSegmentsInfo()
segs.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
NumOfRows: 5,
SchemaVersion: 1,
}})
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{
Version: 3,
},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("non-v3 result rejected", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest",
StorageVersion: storage.StorageV2,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.Error(err)
suite.Nil(infos)
suite.Nil(mutation)
})
suite.Run("v3 success - forward manifest updated", func() {
segs := NewSegmentsInfo()
currentManifest := packed.MarshalManifestPath("/data/segments/1", 1)
resultManifest := packed.MarshalManifestPath("/data/segments/1", 2)
segs.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
Bm25Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(101, 50001)},
NumOfRows: 5,
SchemaVersion: 1,
StorageVersion: storage.StorageV3,
ManifestPath: currentManifest,
}})
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: resultManifest,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.Equal(resultManifest, infos[0].GetManifestPath())
suite.EqualValues(3, infos[0].GetStorageVersion())
suite.EqualValues(3, infos[0].GetSchemaVersion())
suite.EqualValues(1, infos[0].GetDataVersion())
fieldIDs := make(map[int64]bool)
for _, fl := range infos[0].GetBm25Statslogs() {
fieldIDs[fl.GetFieldID()] = true
}
suite.True(fieldIDs[101], "field 101 bm25 stats should be preserved")
suite.False(fieldIDs[102], "field 102 bm25 stats should NOT be added for V3 path")
})
suite.Run("v3 same manifest and newer task schema accepted", func() {
manifestPath := packed.MarshalManifestPath("/data/segments/1", 10)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.SchemaVersion = 1
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = manifestPath
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{
Version: 3,
},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: segment.GetBinlogs(),
Manifest: manifestPath,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.Equal(manifestPath, infos[0].GetManifestPath())
suite.EqualValues(3, infos[0].GetSchemaVersion())
})
suite.Run("v3 same manifest and same task schema accepted", func() {
manifestPath := packed.MarshalManifestPath("/data/segments/1", 10)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.SchemaVersion = 3
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = manifestPath
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: segment.GetBinlogs(),
Manifest: manifestPath,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.Equal(manifestPath, infos[0].GetManifestPath())
suite.EqualValues(3, infos[0].GetSchemaVersion())
suite.EqualValues(0, infos[0].GetDataVersion())
})
suite.Run("input manifest missing rejected", func() {
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.StorageVersion = storage.StorageV3
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: packed.MarshalManifestPath("/data/segments/1", 11),
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
suite.Empty(m.segments.GetSegment(1).GetManifestPath())
})
suite.Run("replacement zero row result is dropped", func() {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 0,
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.Equal(commonpb.SegmentState_Dropped, infos[0].GetState())
suite.Equal(commonpb.SegmentState_Dropped, m.segments.GetSegment(2).GetState())
suite.Equal(commonpb.SegmentState_Dropped, m.segments.GetSegment(1).GetState())
})
suite.Run("catalog error in place does not update memory", func() {
currentManifest := packed.MarshalManifestPath("/data/segments/1", 10)
resultManifest := packed.MarshalManifestPath("/data/segments/1", 11)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = currentManifest
catalogErr := errors.New("catalog error")
metakv := mockkv.NewMetaKv(suite.T())
metakv.EXPECT().MultiSave(mock.Anything, mock.Anything).Return(catalogErr).Once()
m := &meta{
catalog: datacoord.NewCatalog(metakv, "", ""),
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: resultManifest,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, catalogErr)
suite.Nil(infos)
suite.Nil(mutation)
unchanged := m.segments.GetSegment(1)
suite.Equal(currentManifest, unchanged.GetManifestPath())
suite.EqualValues(1, unchanged.GetSchemaVersion())
suite.EqualValues(0, unchanged.GetDataVersion())
})
suite.Run("catalog error replacement does not update memory", func() {
catalogErr := errors.New("catalog error")
metakv := mockkv.NewMetaKv(suite.T())
metakv.EXPECT().HasPrefix(mock.Anything, mock.Anything).Return(false, nil).Times(3)
metakv.EXPECT().MultiSave(mock.Anything, mock.Anything).Return(catalogErr).Once()
m := &meta{
catalog: datacoord.NewCatalog(metakv, "", ""),
segments: makeSegments(1, commonpb.SegmentState_Flushed),
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
PreAllocatedSegmentIDs: &datapb.IDRange{Begin: 2, End: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 2,
NumOfRows: 5,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: "manifest-new",
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, catalogErr)
suite.Nil(infos)
suite.Nil(mutation)
suite.Equal(commonpb.SegmentState_Flushed, m.segments.GetSegment(1).GetState())
suite.Nil(m.segments.GetSegment(2))
})
suite.Run("v3 nil task schema rejected", func() {
currentManifest := packed.MarshalManifestPath("/data/segments/1", 10)
resultManifest := packed.MarshalManifestPath("/data/segments/1", 11)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = currentManifest
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: resultManifest,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
suite.Equal(currentManifest, m.segments.GetSegment(1).GetManifestPath())
})
suite.Run("v3 stale task schema rejected", func() {
currentManifest := packed.MarshalManifestPath("/data/segments/1", 10)
resultManifest := packed.MarshalManifestPath("/data/segments/1", 11)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = currentManifest
segment.SchemaVersion = 3
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 2},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: resultManifest,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
suite.Equal(currentManifest, m.segments.GetSegment(1).GetManifestPath())
})
suite.Run("v3 invisible input rejected", func() {
currentManifest := packed.MarshalManifestPath("/data/segments/1", 10)
resultManifest := packed.MarshalManifestPath("/data/segments/1", 11)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = currentManifest
segment.IsInvisible = true
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: resultManifest,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
suite.Equal(currentManifest, m.segments.GetSegment(1).GetManifestPath())
})
suite.Run("v3 stale manifest rejected", func() {
currentManifest := packed.MarshalManifestPath("/data/segments/1", 10)
resultManifest := packed.MarshalManifestPath("/data/segments/1", 5)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = currentManifest
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: resultManifest,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
suite.Equal(currentManifest, m.segments.GetSegment(1).GetManifestPath())
})
suite.Run("v3 different manifest base rejected", func() {
currentManifest := packed.MarshalManifestPath("/data/segments/1", 10)
resultManifest := packed.MarshalManifestPath("/data/segments/2", 11)
segs := makeSegments(1, commonpb.SegmentState_Flushed)
segment := segs.GetSegment(1)
segment.StorageVersion = storage.StorageV3
segment.ManifestPath = currentManifest
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: resultManifest,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.completeBumpSchemaVersionCompactionMutation(task, result)
suite.ErrorIs(err, merr.ErrIllegalCompactionPlan)
suite.Nil(infos)
suite.Nil(mutation)
suite.Equal(currentManifest, m.segments.GetSegment(1).GetManifestPath())
})
}
func (suite *MetaBasicSuite) TestCompleteCompactionMutation_DispatchesBumpSchemaVersion() {
manifestPath := packed.MarshalManifestPath("/data/segments/1", 10)
segs := NewSegmentsInfo()
segs.SetSegment(1, &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 100,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10000)},
NumOfRows: 5,
SchemaVersion: 1,
StorageVersion: storage.StorageV3,
ManifestPath: manifestPath,
}})
m := &meta{
catalog: &datacoord.Catalog{MetaKv: NewMetaMemoryKV()},
segments: segs,
}
task := &datapb.CompactionTask{
InputSegments: []int64{1},
Type: datapb.CompactionType_BumpSchemaVersionCompaction,
Schema: &schemapb.CollectionSchema{Version: 3},
}
result := &datapb.CompactionPlanResult{
Segments: []*datapb.CompactionSegment{
{
SegmentID: 1,
InsertLogs: []*datapb.FieldBinlog{getFieldBinlogIDs(0, 10001)},
Manifest: manifestPath,
StorageVersion: storage.StorageV3,
},
},
}
infos, mutation, err := m.CompleteCompactionMutation(context.TODO(), task, result)
suite.NoError(err)
suite.NotNil(mutation)
suite.Require().Len(infos, 1)
suite.EqualValues(task.GetSchema().GetVersion(), infos[0].GetSchemaVersion())
}
func TestMeta(t *testing.T) {
suite.Run(t, new(MetaBasicSuite))
suite.Run(t, new(MetaReloadSuite))
}
func TestMeta_Basic(t *testing.T) {
const collID = UniqueID(0)
const partID0 = UniqueID(100)
const partID1 = UniqueID(101)
const channelName = "c1"
// mockAllocator := newMockAllocator(t)
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
testSchema := newTestSchema()
collInfo := &collectionInfo{
ID: collID,
Schema: testSchema,
Partitions: []UniqueID{partID0, partID1},
StartPositions: []*commonpb.KeyDataPair{},
DatabaseName: util.DefaultDBName,
}
collInfoWoPartition := &collectionInfo{
ID: collID,
Schema: testSchema,
Partitions: []UniqueID{},
}
count := atomic.Int64{}
AllocID := func() int64 {
return count.Add(1)
}
t.Run("Test Segment", func(t *testing.T) {
meta.AddCollection(collInfoWoPartition)
// create seg0 for partition0, seg0/seg1 for partition1
segID0_0 := AllocID()
segInfo0_0 := buildSegment(collID, partID0, segID0_0, channelName)
segID1_0 := AllocID()
segInfo1_0 := buildSegment(collID, partID1, segID1_0, channelName)
segID1_1 := AllocID()
segInfo1_1 := buildSegment(collID, partID1, segID1_1, channelName)
// check AddSegment
err = meta.AddSegment(context.TODO(), segInfo0_0)
assert.NoError(t, err)
err = meta.AddSegment(context.TODO(), segInfo1_0)
assert.NoError(t, err)
err = meta.AddSegment(context.TODO(), segInfo1_1)
assert.NoError(t, err)
// check GetSegment
info0_0 := meta.GetHealthySegment(context.TODO(), segID0_0)
assert.NotNil(t, info0_0)
assert.True(t, proto.Equal(info0_0, segInfo0_0))
info1_0 := meta.GetHealthySegment(context.TODO(), segID1_0)
assert.NotNil(t, info1_0)
assert.True(t, proto.Equal(info1_0, segInfo1_0))
// check GetSegmentsOfCollection
segIDs := meta.GetSegmentsIDOfCollection(context.TODO(), collID)
assert.EqualValues(t, 3, len(segIDs))
assert.Contains(t, segIDs, segID0_0)
assert.Contains(t, segIDs, segID1_0)
assert.Contains(t, segIDs, segID1_1)
// check GetSegmentsOfPartition
segIDs = meta.GetSegmentsIDOfPartition(context.TODO(), collID, partID0)
assert.EqualValues(t, 1, len(segIDs))
assert.Contains(t, segIDs, segID0_0)
segIDs = meta.GetSegmentsIDOfPartition(context.TODO(), collID, partID1)
assert.EqualValues(t, 2, len(segIDs))
assert.Contains(t, segIDs, segID1_0)
assert.Contains(t, segIDs, segID1_1)
// check DropSegment
err = meta.DropSegment(context.TODO(), segID1_0)
assert.NoError(t, err)
segIDs = meta.GetSegmentsIDOfPartition(context.TODO(), collID, partID1)
assert.EqualValues(t, 1, len(segIDs))
assert.Contains(t, segIDs, segID1_1)
err = meta.SetState(context.TODO(), segID0_0, commonpb.SegmentState_Sealed)
assert.NoError(t, err)
err = meta.SetState(context.TODO(), segID0_0, commonpb.SegmentState_Flushed)
assert.NoError(t, err)
info0_0 = meta.GetHealthySegment(context.TODO(), segID0_0)
assert.NotNil(t, info0_0)
assert.EqualValues(t, commonpb.SegmentState_Flushed, info0_0.State)
})
t.Run("Test segment with kv fails", func(t *testing.T) {
// inject error for `Save`
metakv := mockkv.NewMetaKv(t)
metakv.EXPECT().Save(mock.Anything, mock.Anything, mock.Anything).Return(errors.New("failed")).Maybe()
metakv.EXPECT().MultiSave(mock.Anything, mock.Anything).Return(errors.New("failed")).Maybe()
metakv.EXPECT().WalkWithPrefix(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
metakv.EXPECT().LoadWithPrefix(mock.Anything, mock.Anything).Return(nil, nil, nil).Maybe()
metakv.EXPECT().Has(mock.Anything, datacoord.FileResourceVersionKey).Return(false, nil).Maybe()
catalog := datacoord.NewCatalog(metakv, "", "")
broker := broker.NewMockBroker(t)
broker.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
meta, err := newMeta(context.TODO(), catalog, nil, broker)
assert.NoError(t, err)
err = meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{}))
assert.Error(t, err)
metakv2 := mockkv.NewMetaKv(t)
metakv2.EXPECT().Save(mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
metakv2.EXPECT().Has(mock.Anything, datacoord.FileResourceVersionKey).Return(false, nil).Maybe()
metakv2.EXPECT().MultiSave(mock.Anything, mock.Anything).Return(nil).Maybe()
metakv2.EXPECT().Remove(mock.Anything, mock.Anything).Return(errors.New("failed")).Maybe()
metakv2.EXPECT().MultiRemove(mock.Anything, mock.Anything).Return(errors.New("failed")).Maybe()
metakv2.EXPECT().WalkWithPrefix(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
metakv2.EXPECT().LoadWithPrefix(mock.Anything, mock.Anything).Return(nil, nil, nil).Maybe()
metakv2.EXPECT().MultiSaveAndRemoveWithPrefix(mock.Anything, mock.Anything, mock.Anything).Return(errors.New("failed"))
catalog = datacoord.NewCatalog(metakv2, "", "")
meta, err = newMeta(context.TODO(), catalog, nil, broker)
assert.NoError(t, err)
// nil, since no segment yet
err = meta.DropSegment(context.TODO(), 0)
assert.NoError(t, err)
// nil, since Save error not injected
err = meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{}))
assert.NoError(t, err)
// error injected
err = meta.DropSegment(context.TODO(), 0)
assert.Error(t, err)
catalog = datacoord.NewCatalog(metakv, "", "")
meta, err = newMeta(context.TODO(), catalog, nil, broker)
assert.NoError(t, err)
assert.NotNil(t, meta)
})
t.Run("Test GetCount", func(t *testing.T) {
const rowCount0 = 100
const rowCount1 = 300
// no segment
nums := meta.GetNumRowsOfCollection(context.Background(), collID)
assert.EqualValues(t, 0, nums)
// add seg1 with 100 rows
segID0 := AllocID()
segInfo0 := buildSegment(collID, partID0, segID0, channelName)
segInfo0.NumOfRows = rowCount0
err = meta.AddSegment(context.TODO(), segInfo0)
assert.NoError(t, err)
// add seg2 with 300 rows
segID1 := AllocID()
segInfo1 := buildSegment(collID, partID0, segID1, channelName)
segInfo1.NumOfRows = rowCount1
err = meta.AddSegment(context.TODO(), segInfo1)
assert.NoError(t, err)
// check partition/collection statistics
nums = meta.GetNumRowsOfPartition(context.TODO(), collID, partID0)
assert.EqualValues(t, (rowCount0 + rowCount1), nums)
nums = meta.GetNumRowsOfCollection(context.Background(), collID)
assert.EqualValues(t, (rowCount0 + rowCount1), nums)
})
t.Run("Test GetSegmentsChanPart", func(t *testing.T) {
result := GetSegmentsChanPart(meta, collID, SegmentFilterFunc(func(segment *SegmentInfo) bool { return true }))
assert.Equal(t, 2, len(result))
for _, entry := range result {
assert.Equal(t, "c1", entry.channelName)
if entry.partitionID == UniqueID(100) {
assert.Equal(t, 3, len(entry.segments))
}
if entry.partitionID == UniqueID(101) {
assert.Equal(t, 1, len(entry.segments))
}
}
result = GetSegmentsChanPart(meta, 10)
assert.Equal(t, 0, len(result))
})
t.Run("GetClonedCollectionInfo", func(t *testing.T) {
// collection does not exist
ret := meta.GetClonedCollectionInfo(-1)
assert.Nil(t, ret)
collInfo.Properties = map[string]string{
common.CollectionTTLConfigKey: "3600",
}
meta.AddCollection(collInfo)
ret = meta.GetClonedCollectionInfo(collInfo.ID)
equalCollectionInfo(t, collInfo, ret)
collInfo.StartPositions = []*commonpb.KeyDataPair{
{
Key: "k",
Data: []byte("v"),
},
}
meta.AddCollection(collInfo)
ret = meta.GetClonedCollectionInfo(collInfo.ID)
equalCollectionInfo(t, collInfo, ret)
})
t.Run("Test GetCollectionBinlogSize", func(t *testing.T) {
const size0 = 1024
const size1 = 2048
// add seg0 with size0
segID0 := AllocID()
segInfo0 := buildSegment(collID, partID0, segID0, channelName)
segInfo0.size.Store(size0)
err = meta.AddSegment(context.TODO(), segInfo0)
assert.NoError(t, err)
// add seg1 with size1
segID1 := AllocID()
segInfo1 := buildSegment(collID, partID0, segID1, channelName)
segInfo1.size.Store(size1)
err = meta.AddSegment(context.TODO(), segInfo1)
assert.NoError(t, err)
// check TotalBinlogSize
quotaInfo := meta.GetQuotaInfo()
assert.Len(t, quotaInfo.CollectionBinlogSize, 1)
assert.Equal(t, int64(size0+size1), quotaInfo.CollectionBinlogSize[collID])
assert.Equal(t, int64(size0+size1), quotaInfo.TotalBinlogSize)
meta.collections.Insert(collID, collInfo)
quotaInfo = meta.GetQuotaInfo()
assert.Len(t, quotaInfo.CollectionBinlogSize, 1)
assert.Equal(t, int64(size0+size1), quotaInfo.CollectionBinlogSize[collID])
assert.Equal(t, int64(size0+size1), quotaInfo.TotalBinlogSize)
})
t.Run("Test AddAllocation", func(t *testing.T) {
meta, _ := newMemoryMeta(t)
err := meta.AddAllocation(1, &Allocation{
SegmentID: 1,
NumOfRows: 1,
ExpireTime: 0,
})
assert.Error(t, err)
})
}
func TestGetUnFlushedSegments(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
s1 := &datapb.SegmentInfo{
ID: 0,
CollectionID: 0,
PartitionID: 0,
State: commonpb.SegmentState_Growing,
}
err = meta.AddSegment(context.TODO(), NewSegmentInfo(s1))
assert.NoError(t, err)
s2 := &datapb.SegmentInfo{
ID: 1,
CollectionID: 0,
PartitionID: 0,
State: commonpb.SegmentState_Flushed,
}
err = meta.AddSegment(context.TODO(), NewSegmentInfo(s2))
assert.NoError(t, err)
segments := meta.GetUnFlushedSegments()
assert.NoError(t, err)
assert.EqualValues(t, 1, len(segments))
assert.EqualValues(t, 0, segments[0].ID)
assert.NotEqualValues(t, commonpb.SegmentState_Flushed, segments[0].State)
}
func TestAlterSegmentsWithRecovery(t *testing.T) {
rootPath := "AlterSegmentsWithRecovery-" + funcutil.RandomString(10)
meta, _ := newMetaWithEtcd(t, rootPath)
etcdCli, _ := kvfactory.GetEtcdAndPath()
collectionID := int64(1)
partitionID := int64(1)
segmentID := int64(1)
fieldID := int64(1)
segment1 := NewSegmentInfo(&datapb.SegmentInfo{
CollectionID: collectionID,
PartitionID: partitionID,
ID: segmentID,
State: commonpb.SegmentState_Growing,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 334)},
Deltalogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 100)},
Bm25Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 20)},
})
err := meta.AddSegment(context.TODO(), segment1)
require.NoError(t, err)
segmentPath := fmt.Sprintf("%s/%s/%d/%d/%d", rootPath, datacoord.SegmentPrefix, collectionID, partitionID, segmentID)
binlogsPath := fmt.Sprintf("%s/%s/%d/%d/%d/%d", rootPath, datacoord.SegmentBinlogPathPrefix, collectionID, partitionID, segmentID, fieldID)
statslogsPath := fmt.Sprintf("%s/%s/%d/%d/%d/%d", rootPath, datacoord.SegmentStatslogPathPrefix, collectionID, partitionID, segmentID, fieldID)
deltalogsPath := fmt.Sprintf("%s/%s/%d/%d/%d/%d", rootPath, datacoord.SegmentDeltalogPathPrefix, collectionID, partitionID, segmentID, fieldID)
bm25StatslogsPath := fmt.Sprintf("%s/%s/%d/%d/%d/%d", rootPath, datacoord.SegmentBM25logPathPrefix, collectionID, partitionID, segmentID, fieldID)
checkVersion := func(sVersion int64, bVersion int64, stVersion int64, dVersion int64, bm25Version int64) {
kv, err := etcdCli.Get(context.TODO(), segmentPath)
require.NoError(t, err)
require.Equal(t, kv.Kvs[0].Version, sVersion)
kv, err = etcdCli.Get(context.TODO(), binlogsPath)
require.NoError(t, err)
require.Equal(t, kv.Kvs[0].Version, bVersion)
kv, err = etcdCli.Get(context.TODO(), statslogsPath)
require.NoError(t, err)
require.Equal(t, kv.Kvs[0].Version, stVersion)
kv, err = etcdCli.Get(context.TODO(), deltalogsPath)
require.NoError(t, err)
require.Equal(t, kv.Kvs[0].Version, dVersion)
kv, err = etcdCli.Get(context.TODO(), bm25StatslogsPath)
require.NoError(t, err)
require.Equal(t, kv.Kvs[0].Version, bm25Version)
}
checkVersion(1, 1, 1, 1, 1)
err = meta.UpdateSegmentsInfo(context.TODO(), AddBinlogsOperator(1,
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
nil,
nil,
nil,
))
require.NoError(t, err)
checkVersion(2, 2, 1, 1, 1)
err = meta.UpdateSegmentsInfo(context.TODO(), AddBinlogsOperator(1,
nil,
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
nil,
nil,
))
require.NoError(t, err)
checkVersion(3, 2, 2, 1, 1)
err = meta.UpdateSegmentsInfo(context.TODO(), AddBinlogsOperator(1,
nil,
nil,
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
nil,
))
require.NoError(t, err)
checkVersion(4, 2, 2, 2, 1)
err = meta.UpdateSegmentsInfo(context.TODO(), AddBinlogsOperator(1,
nil,
nil,
nil,
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
))
require.NoError(t, err)
checkVersion(5, 2, 2, 2, 2)
err = meta.UpdateSegmentsInfo(context.TODO(), AddBinlogsOperator(1,
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 10, 333)},
))
require.NoError(t, err)
checkVersion(6, 3, 3, 3, 3)
}
func TestAddL0DeltalogsAndUpdateManifestOperator(t *testing.T) {
basePath := "/tmp/milvus/insert_log/1/10/200"
oldManifest := packed.MarshalManifestPath(basePath, 7)
newManifest := packed.MarshalManifestPath(basePath, 8)
meta, err := newMemoryMeta(t)
require.NoError(t, err)
require.NoError(t, meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 200,
CollectionID: 1,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
ManifestPath: oldManifest,
})))
deltalogs := []*datapb.FieldBinlog{{
Binlogs: []*datapb.Binlog{{
LogID: 9001,
LogPath: basePath + "/_delta/9001",
EntriesNum: 3,
MemorySize: 128,
}},
}}
patch := mockey.Mock(packed.AddDeltaLogsToManifestOverwrite).To(
func(manifestPath string, storageConfig *indexpb.StorageConfig, deltaLogs []packed.DeltaLogEntry) (string, error) {
require.Equal(t, oldManifest, manifestPath)
require.NotNil(t, storageConfig)
require.Len(t, deltaLogs, 1)
require.Equal(t, basePath+"/_delta/9001", deltaLogs[0].Path)
require.EqualValues(t, 3, deltaLogs[0].NumEntries)
return newManifest, nil
},
).Build()
defer patch.UnPatch()
cache := make(map[int64]string)
err = meta.UpdateSegmentsInfo(context.TODO(), AddL0DeltalogsAndUpdateManifestOperator(
200,
deltalogs,
&indexpb.StorageConfig{},
cache,
))
require.NoError(t, err)
updated := meta.GetSegment(context.TODO(), 200)
require.Equal(t, newManifest, updated.GetManifestPath())
require.Equal(t, newManifest, cache[int64(200)])
require.Len(t, updated.GetDeltalogs(), 1)
require.Len(t, updated.GetDeltalogs()[0].GetBinlogs(), 1)
require.Empty(t, updated.GetDeltalogs()[0].GetBinlogs()[0].GetLogPath())
require.EqualValues(t, 9001, updated.GetDeltalogs()[0].GetBinlogs()[0].GetLogID())
require.EqualValues(t, 3, updated.GetDeltalogs()[0].GetBinlogs()[0].GetEntriesNum())
}
func TestAddL0DeltalogsAndUpdateManifestOperatorCommitsManifestsConcurrently(t *testing.T) {
basePath1 := "/tmp/milvus/insert_log/1/10/200"
basePath2 := "/tmp/milvus/insert_log/1/10/201"
oldManifest1 := packed.MarshalManifestPath(basePath1, 7)
oldManifest2 := packed.MarshalManifestPath(basePath2, 11)
newManifest1 := packed.MarshalManifestPath(basePath1, 8)
newManifest2 := packed.MarshalManifestPath(basePath2, 12)
meta, err := newMemoryMeta(t)
require.NoError(t, err)
require.NoError(t, meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 200,
CollectionID: 1,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
ManifestPath: oldManifest1,
})))
require.NoError(t, meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 201,
CollectionID: 1,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
ManifestPath: oldManifest2,
})))
paramtable.Get().Save(paramtable.Get().DataCoordCfg.L0ManifestUpdatePoolSize.Key, "2")
defer paramtable.Get().Reset(paramtable.Get().DataCoordCfg.L0ManifestUpdatePoolSize.Key)
entered := make(chan string, 2)
release := make(chan struct{})
patch := mockey.Mock(packed.AddDeltaLogsToManifestOverwrite).To(
func(manifestPath string, storageConfig *indexpb.StorageConfig, deltaLogs []packed.DeltaLogEntry) (string, error) {
entered <- manifestPath
<-release
switch manifestPath {
case oldManifest1:
return newManifest1, nil
case oldManifest2:
return newManifest2, nil
default:
require.Failf(t, "unexpected manifest", manifestPath)
return "", nil
}
},
).Build()
defer patch.UnPatch()
errCh := make(chan error, 1)
go func() {
errCh <- meta.UpdateSegmentsInfo(context.TODO(),
AddL0DeltalogsAndUpdateManifestOperator(200, []*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{LogID: 9001, LogPath: basePath1 + "/_delta/9001", EntriesNum: 3}}}}, &indexpb.StorageConfig{}, nil),
AddL0DeltalogsAndUpdateManifestOperator(201, []*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{LogID: 9002, LogPath: basePath2 + "/_delta/9002", EntriesNum: 5}}}}, &indexpb.StorageConfig{}, nil),
)
}()
got := map[string]struct{}{}
for i := 0; i < 2; i++ {
select {
case manifestPath := <-entered:
got[manifestPath] = struct{}{}
case <-time.After(time.Second):
require.FailNow(t, "manifest updates did not run concurrently")
}
}
require.Contains(t, got, oldManifest1)
require.Contains(t, got, oldManifest2)
close(release)
require.NoError(t, <-errCh)
updated1 := meta.GetSegment(context.TODO(), 200)
require.Equal(t, newManifest1, updated1.GetManifestPath())
require.Empty(t, updated1.GetDeltalogs()[0].GetBinlogs()[0].GetLogPath())
updated2 := meta.GetSegment(context.TODO(), 201)
require.Equal(t, newManifest2, updated2.GetManifestPath())
require.Empty(t, updated2.GetDeltalogs()[0].GetBinlogs()[0].GetLogPath())
}
func TestAddL0DeltalogsAndUpdateManifestOperatorSerializesConcurrentUpdates(t *testing.T) {
basePath := "/tmp/milvus/insert_log/1/10/200"
oldManifest := packed.MarshalManifestPath(basePath, 7)
manifest8 := packed.MarshalManifestPath(basePath, 8)
manifest9 := packed.MarshalManifestPath(basePath, 9)
meta, err := newMemoryMeta(t)
require.NoError(t, err)
require.NoError(t, meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 200,
CollectionID: 1,
PartitionID: 10,
State: commonpb.SegmentState_Flushed,
ManifestPath: oldManifest,
})))
var mu sync.Mutex
calls := make([]string, 0, 2)
patch := mockey.Mock(packed.AddDeltaLogsToManifestOverwrite).To(
func(manifestPath string, storageConfig *indexpb.StorageConfig, deltaLogs []packed.DeltaLogEntry) (string, error) {
mu.Lock()
defer mu.Unlock()
calls = append(calls, manifestPath)
if len(calls) == 1 {
return manifest8, nil
}
return manifest9, nil
},
).Build()
defer patch.UnPatch()
makeDelta := func(logID int64) []*datapb.FieldBinlog {
return []*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{
LogID: logID,
LogPath: fmt.Sprintf("%s/_delta/%d", basePath, logID),
EntriesNum: 1,
}}}}
}
errs := make(chan error, 2)
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
errs <- meta.UpdateSegmentsInfo(context.TODO(), AddL0DeltalogsAndUpdateManifestOperator(200, makeDelta(9001), &indexpb.StorageConfig{}, nil))
}()
go func() {
defer wg.Done()
errs <- meta.UpdateSegmentsInfo(context.TODO(), AddL0DeltalogsAndUpdateManifestOperator(200, makeDelta(9002), &indexpb.StorageConfig{}, nil))
}()
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
require.Equal(t, []string{oldManifest, manifest8}, calls)
updated := meta.GetSegment(context.TODO(), 200)
require.Equal(t, manifest9, updated.GetManifestPath())
require.Len(t, updated.GetDeltalogs(), 1)
require.Len(t, updated.GetDeltalogs()[0].GetBinlogs(), 2)
}
func TestAddL0DeltalogsAndUpdateManifestOperatorRequiresLogPath(t *testing.T) {
basePath := "/tmp/milvus/insert_log/1/10/200"
oldManifest := packed.MarshalManifestPath(basePath, 7)
meta, err := newMemoryMeta(t)
require.NoError(t, err)
require.NoError(t, meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 200,
State: commonpb.SegmentState_Flushed,
ManifestPath: oldManifest,
})))
err = meta.UpdateSegmentsInfo(context.TODO(), AddL0DeltalogsAndUpdateManifestOperator(
200,
[]*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{LogID: 9001, EntriesNum: 3}}}},
&indexpb.StorageConfig{},
nil,
))
require.Error(t, err)
require.Contains(t, err.Error(), "missing deltalog path")
updated := meta.GetSegment(context.TODO(), 200)
require.Equal(t, oldManifest, updated.GetManifestPath())
require.Empty(t, updated.GetDeltalogs())
}
func TestAddL0DeltalogsAndUpdateManifestOperatorCacheDoesNotRegressManifest(t *testing.T) {
basePath := "/tmp/milvus/insert_log/1/10/200"
manifest8 := packed.MarshalManifestPath(basePath, 8)
manifest9 := packed.MarshalManifestPath(basePath, 9)
meta, err := newMemoryMeta(t)
require.NoError(t, err)
require.NoError(t, meta.AddSegment(context.TODO(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 200,
State: commonpb.SegmentState_Flushed,
ManifestPath: manifest9,
})))
calls := 0
patch := mockey.Mock(packed.AddDeltaLogsToManifestOverwrite).To(
func(manifestPath string, storageConfig *indexpb.StorageConfig, deltaLogs []packed.DeltaLogEntry) (string, error) {
calls++
return "", errors.New("should not be called")
},
).Build()
defer patch.UnPatch()
cache := map[int64]string{200: manifest8}
err = meta.UpdateSegmentsInfo(context.TODO(), AddL0DeltalogsAndUpdateManifestOperator(
200,
[]*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{LogID: 9001, LogPath: basePath + "/_delta/9001", EntriesNum: 3}}}},
&indexpb.StorageConfig{},
cache,
))
require.NoError(t, err)
require.Zero(t, calls)
updated := meta.GetSegment(context.TODO(), 200)
require.Equal(t, manifest9, updated.GetManifestPath())
require.Len(t, updated.GetDeltalogs(), 1)
}
func TestUpdateSegmentsInfo(t *testing.T) {
t.Run("operator error stops update", func(t *testing.T) {
meta, err := newMemoryMeta(t)
require.NoError(t, err)
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
})
require.NoError(t, meta.AddSegment(context.TODO(), segment))
expectedErr := errors.New("operator failed")
err = meta.UpdateSegmentsInfo(
context.TODO(),
func(pack *updateSegmentPack) bool {
pack.err = expectedErr
return false
},
UpdateStatusOperator(1, commonpb.SegmentState_Dropped),
)
require.ErrorIs(t, err, expectedErr)
updated := meta.GetSegment(context.TODO(), 1)
require.Equal(t, commonpb.SegmentState_Flushed, updated.GetState())
})
t.Run("normal", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
segment1 := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1, State: commonpb.SegmentState_Growing,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDsWithEntry(1, 1, 222)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 2)},
})
err = meta.AddSegment(context.TODO(), segment1)
assert.NoError(t, err)
require.EqualValues(t, -1, segment1.deltaRowcount.Load())
assert.EqualValues(t, 0, segment1.getDeltaCount())
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Growing),
AddBinlogsOperator(1,
[]*datapb.FieldBinlog{getFieldBinlogIDsWithEntry(1, 10, 333)},
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 334)},
[]*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{EntriesNum: 1, TimestampFrom: 100, TimestampTo: 200, LogSize: 1000, LogID: 335}}}},
[]*datapb.FieldBinlog{},
),
UpdateStartPosition([]*datapb.SegmentStartPosition{{SegmentID: 1, StartPosition: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}}}}),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 10}}),
)
assert.NoError(t, err)
updated := meta.GetHealthySegment(context.TODO(), 1)
assert.EqualValues(t, -1, updated.deltaRowcount.Load())
assert.EqualValues(t, 1, updated.getDeltaCount())
expected := &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, State: commonpb.SegmentState_Growing, NumOfRows: 11,
StartPosition: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}},
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 222, 333)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 0, 1)},
Deltalogs: []*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{EntriesNum: 1, TimestampFrom: 100, TimestampTo: 200, LogSize: 1000}}}},
}}
assert.Equal(t, updated.StartPosition, expected.StartPosition)
assert.Equal(t, updated.DmlPosition, expected.DmlPosition)
assert.Equal(t, updated.DmlPosition, expected.DmlPosition)
assert.Equal(t, len(updated.Binlogs[0].Binlogs), len(expected.Binlogs[0].Binlogs))
assert.Equal(t, len(updated.Statslogs[0].Binlogs), len(expected.Statslogs[0].Binlogs))
assert.Equal(t, len(updated.Deltalogs[0].Binlogs), len(expected.Deltalogs[0].Binlogs))
assert.Equal(t, updated.State, expected.State)
assert.Equal(t, updated.size.Load(), expected.size.Load())
assert.Equal(t, updated.NumOfRows, expected.NumOfRows)
})
t.Run("update binlogs from save binlog paths", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
segment1 := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1, State: commonpb.SegmentState_Growing,
Binlogs: []*datapb.FieldBinlog{},
Statslogs: []*datapb.FieldBinlog{},
})
err = meta.AddSegment(context.TODO(), segment1)
assert.NoError(t, err)
require.EqualValues(t, -1, segment1.deltaRowcount.Load())
assert.EqualValues(t, 0, segment1.getDeltaCount())
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Growing),
UpdateBinlogsFromSaveBinlogPathsOperator(1,
[]*datapb.FieldBinlog{getFieldBinlogIDsWithEntry(1, 10, 333)},
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 334)},
[]*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{EntriesNum: 1, TimestampFrom: 100, TimestampTo: 200, LogSize: 1000, LogID: 335}}}},
[]*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{EntriesNum: 1, TimestampFrom: 100, TimestampTo: 200, LogSize: 1000, LogID: 335}}}},
),
UpdateStartPosition([]*datapb.SegmentStartPosition{{SegmentID: 1, StartPosition: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}}}}),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 10, Position: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 100}}}, true),
UpdateManifest(1, "files/binlogs/1/2/1000/manifest_0"),
)
assert.NoError(t, err)
updated := meta.GetHealthySegment(context.TODO(), 1)
assert.EqualValues(t, -1, updated.deltaRowcount.Load())
assert.EqualValues(t, 1, updated.getDeltaCount())
assert.Equal(t, updated.StartPosition, &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}})
assert.Equal(t, updated.DmlPosition, &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 100})
assert.Equal(t, len(updated.Binlogs[0].Binlogs), 1)
assert.Equal(t, len(updated.Statslogs[0].Binlogs), 1)
assert.Equal(t, len(updated.Deltalogs[0].Binlogs), 1)
assert.Equal(t, len(updated.Bm25Statslogs[0].Binlogs), 1)
assert.Equal(t, updated.State, commonpb.SegmentState_Growing)
assert.Equal(t, updated.NumOfRows, int64(10))
assert.Equal(t, updated.ManifestPath, "files/binlogs/1/2/1000/manifest_0")
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Growing),
UpdateBinlogsFromSaveBinlogPathsOperator(1, nil, nil, nil, nil),
UpdateStartPosition([]*datapb.SegmentStartPosition{{SegmentID: 1, StartPosition: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}}}}),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 10, Position: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 99}}}, true),
)
assert.NoError(t, err) // stale update is swallowed as a benign no-op; segment must stay unchanged below
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Growing),
UpdateBinlogsFromSaveBinlogPathsOperator(1,
[]*datapb.FieldBinlog{getFieldBinlogIDsWithEntry(1, 10, 335, 337)},
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 336)},
[]*datapb.FieldBinlog{},
[]*datapb.FieldBinlog{},
),
UpdateStatusOperator(1, commonpb.SegmentState_Flushed),
UpdateStartPosition([]*datapb.SegmentStartPosition{{SegmentID: 1, StartPosition: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}}}}),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 12, Position: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 101}}}, true),
UpdateManifest(1, "files/binlogs/1/2/1000/manifest_2"),
)
assert.NoError(t, err)
updated = meta.GetHealthySegment(context.TODO(), 1)
assert.Equal(t, updated.NumOfRows, int64(20))
assert.Equal(t, updated.DmlPosition, &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 101})
assert.Equal(t, len(updated.Binlogs[0].Binlogs), 2)
assert.Equal(t, len(updated.Statslogs[0].Binlogs), 1)
assert.Equal(t, len(updated.Deltalogs), 0)
assert.Equal(t, len(updated.Bm25Statslogs), 0)
assert.Equal(t, updated.State, commonpb.SegmentState_Flushed)
assert.Equal(t, updated.ManifestPath, "files/binlogs/1/2/1000/manifest_2")
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Flushed),
UpdateBinlogsFromSaveBinlogPathsOperator(1,
[]*datapb.FieldBinlog{},
[]*datapb.FieldBinlog{},
[]*datapb.FieldBinlog{},
[]*datapb.FieldBinlog{}),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 12, Position: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 101}}}, true),
)
assert.NoError(t, err) // stale update is swallowed as a benign no-op; segment must stay unchanged below
updated = meta.GetHealthySegment(context.TODO(), 1)
assert.Equal(t, updated.NumOfRows, int64(20))
assert.Equal(t, updated.DmlPosition, &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 101})
assert.Equal(t, len(updated.Binlogs[0].Binlogs), 2)
assert.Equal(t, len(updated.Statslogs[0].Binlogs), 1)
assert.Equal(t, len(updated.Deltalogs), 0)
assert.Equal(t, len(updated.Bm25Statslogs), 0)
assert.Equal(t, updated.State, commonpb.SegmentState_Flushed)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Dropped),
UpdateBinlogsFromSaveBinlogPathsOperator(1,
[]*datapb.FieldBinlog{},
[]*datapb.FieldBinlog{},
[]*datapb.FieldBinlog{},
[]*datapb.FieldBinlog{}),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 12, Position: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 101}}}, true),
)
assert.NoError(t, err)
updated = meta.GetSegment(context.TODO(), 1)
assert.Equal(t, updated.State, commonpb.SegmentState_Dropped)
})
t.Run("v3 storage segment with empty binlogs uses checkpoint NumOfRows", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Create a V3 segment with no binlogs (V3 storage uses ManifestPath instead)
segment1 := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Growing,
Binlogs: []*datapb.FieldBinlog{},
Statslogs: []*datapb.FieldBinlog{},
ManifestPath: "files/binlogs/1/2/1000/manifest_0",
})
err = meta.AddSegment(context.TODO(), segment1)
assert.NoError(t, err)
assert.EqualValues(t, 0, segment1.NumOfRows)
// UpdateCheckPointOperator with cpNumRows=100, segment has no binlogs
// CalcRowCountFromBinLog will return 0, so should fall back to cpNumRows
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{
SegmentID: 1,
NumOfRows: 100,
Position: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}, Timestamp: 100},
}}, true),
)
assert.NoError(t, err)
updated := meta.GetHealthySegment(context.TODO(), 1)
// NumOfRows should be set from checkpoint, not left at 0
assert.EqualValues(t, 100, updated.NumOfRows)
})
t.Run("update compacted segment", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// segment not found
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateCompactedOperator(1),
)
assert.NoError(t, err)
// normal
segment1 := &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 1, State: commonpb.SegmentState_Flushed,
Binlogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 2)},
Statslogs: []*datapb.FieldBinlog{getFieldBinlogIDs(1, 2)},
}}
err = meta.AddSegment(context.TODO(), segment1)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateCompactedOperator(1),
)
assert.NoError(t, err)
})
t.Run("update non-existed segment", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Flushing),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
AddBinlogsOperator(1, nil, nil, nil, nil),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStartPosition([]*datapb.SegmentStartPosition{{SegmentID: 1, StartPosition: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}}}}),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 10}}),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateBinlogsOperator(1, nil, nil, nil, nil),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateDmlPosition(1, nil),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateDmlPosition(1, &msgpb.MsgPosition{MsgID: []byte{1}}),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateImportedRows(1, 0),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateIsImporting(1, true),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateManifest(1, "files/binlogs/1/2/1000/manifest_0"),
)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(context.TODO(), UpdateAsDroppedIfEmptyWhenFlushing(1))
assert.NoError(t, err)
})
t.Run("update empty segment into flush", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
meta.AddSegment(context.Background(), &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{ID: 1, State: commonpb.SegmentState_Growing}})
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Flushing),
UpdateAsDroppedIfEmptyWhenFlushing(1),
)
assert.NoError(t, err)
})
t.Run("update checkpoints and start position of non existed segment", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
segment1 := &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{ID: 1, State: commonpb.SegmentState_Growing}}
err = meta.AddSegment(context.TODO(), segment1)
assert.NoError(t, err)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 2, NumOfRows: 10}}),
)
assert.NoError(t, err)
assert.Nil(t, meta.GetHealthySegment(context.TODO(), 2))
})
t.Run("test save etcd failed", func(t *testing.T) {
metakv := mockkv.NewMetaKv(t)
metakv.EXPECT().Save(mock.Anything, mock.Anything, mock.Anything).Return(errors.New("mocked fail")).Maybe()
metakv.EXPECT().MultiSave(mock.Anything, mock.Anything).Return(errors.New("mocked fail")).Maybe()
metakv.EXPECT().WalkWithPrefix(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
metakv.EXPECT().LoadWithPrefix(mock.Anything, mock.Anything).Return(nil, nil, nil).Maybe()
metakv.EXPECT().Has(mock.Anything, mock.Anything).Return(false, nil).Maybe()
catalog := datacoord.NewCatalog(metakv, "", "")
broker := broker.NewMockBroker(t)
broker.EXPECT().ShowCollectionIDs(mock.Anything).Return(nil, nil)
meta, err := newMeta(context.TODO(), catalog, nil, broker)
assert.NoError(t, err)
segmentInfo := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
NumOfRows: 0,
State: commonpb.SegmentState_Growing,
},
}
meta.segments.SetSegment(1, segmentInfo)
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Flushing),
AddBinlogsOperator(1,
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 2)},
[]*datapb.FieldBinlog{getFieldBinlogIDs(1, 2)},
[]*datapb.FieldBinlog{{Binlogs: []*datapb.Binlog{{EntriesNum: 1, TimestampFrom: 100, TimestampTo: 200, LogSize: 1000, LogPath: "", LogID: 2}}}},
[]*datapb.FieldBinlog{},
),
UpdateStartPosition([]*datapb.SegmentStartPosition{{SegmentID: 1, StartPosition: &msgpb.MsgPosition{MsgID: []byte{1, 2, 3}}}}),
UpdateCheckPointOperator(1, []*datapb.CheckPoint{{SegmentID: 1, NumOfRows: 10}}),
)
assert.Error(t, err)
assert.Equal(t, "mocked fail", err.Error())
segmentInfo = meta.GetHealthySegment(context.TODO(), 1)
assert.EqualValues(t, 0, segmentInfo.NumOfRows)
assert.Equal(t, commonpb.SegmentState_Growing, segmentInfo.State)
assert.Nil(t, segmentInfo.Binlogs)
assert.Nil(t, segmentInfo.StartPosition)
})
t.Run("update commit timestamp", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
meta.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{ID: 1, State: commonpb.SegmentState_Flushed},
})
const testTs uint64 = 1234567890
err = meta.UpdateSegmentsInfo(context.TODO(), UpdateCommitTimestamp(1, testTs))
assert.NoError(t, err)
seg := meta.GetSegment(context.TODO(), 1)
assert.Equal(t, testTs, seg.GetCommitTimestamp())
// verify clearing to zero works
err = meta.UpdateSegmentsInfo(context.TODO(), UpdateCommitTimestamp(1, 0))
assert.NoError(t, err)
seg = meta.GetSegment(context.TODO(), 1)
assert.Equal(t, uint64(0), seg.GetCommitTimestamp())
})
}
func TestSegmentMetricFormatLabel(t *testing.T) {
tests := []struct {
name string
segment *SegmentInfo
want string
}{
{
name: "legacy storage without format",
segment: NewSegmentInfo(&datapb.SegmentInfo{
StorageVersion: storage.StorageV1,
}),
want: "legacy",
},
{
name: "storage v2 without format",
segment: NewSegmentInfo(&datapb.SegmentInfo{
StorageVersion: storage.StorageV2,
}),
want: "unknown",
},
{
name: "storage v3 parquet",
segment: NewSegmentInfo(&datapb.SegmentInfo{
StorageVersion: storage.StorageV3,
Binlogs: []*datapb.FieldBinlog{
{Format: "parquet"},
{Format: "parquet"},
},
}),
want: "parquet",
},
{
name: "storage v3 external iceberg table",
segment: NewSegmentInfo(&datapb.SegmentInfo{
StorageVersion: storage.StorageV3,
Binlogs: []*datapb.FieldBinlog{
{Format: "iceberg-table"},
},
}),
want: "iceberg-table",
},
{
name: "storage v3 external lance table",
segment: NewSegmentInfo(&datapb.SegmentInfo{
StorageVersion: storage.StorageV3,
Binlogs: []*datapb.FieldBinlog{
{Format: "lance-table"},
},
}),
want: "lance-table",
},
{
name: "mixed column group formats",
segment: NewSegmentInfo(&datapb.SegmentInfo{
StorageVersion: storage.StorageV3,
Binlogs: []*datapb.FieldBinlog{
{Format: "parquet"},
{Format: "vortex"},
},
}),
want: "mixed",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.want, segmentMetricFormatLabel(test.segment))
})
}
}
func TestUpdateSegmentsInfoUpdatesSegmentFormatMetric(t *testing.T) {
metrics.DataCoordNumSegments.Reset()
meta, err := newMemoryMeta(t)
require.NoError(t, err)
defer metrics.DataCoordNumSegments.Reset()
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
StorageVersion: storage.StorageV3,
})
require.NoError(t, meta.AddSegment(context.TODO(), segment))
unknownLabels := []string{metrics.FlushedSegmentLabel, datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), "unknown"}
icebergLabels := []string{metrics.FlushedSegmentLabel, datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), "iceberg-table"}
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(unknownLabels...)))
err = meta.UpdateSegmentsInfo(context.TODO(), AddBinlogsOperator(1,
[]*datapb.FieldBinlog{
{
FieldID: 100,
ChildFields: []int64{100},
Format: "iceberg-table",
Binlogs: []*datapb.Binlog{
{LogID: 10, EntriesNum: 100, LogSize: 1000, MemorySize: 1000},
},
},
},
nil,
nil,
nil,
))
require.NoError(t, err)
assert.Equal(t, float64(0), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(unknownLabels...)))
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(icebergLabels...)))
}
func TestUpdateSegmentsInfoUpdatesSegmentFormatMetricToMixed(t *testing.T) {
metrics.DataCoordNumSegments.Reset()
meta, err := newMemoryMeta(t)
require.NoError(t, err)
defer metrics.DataCoordNumSegments.Reset()
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
StorageVersion: storage.StorageV3,
})
require.NoError(t, meta.AddSegment(context.TODO(), segment))
unknownLabels := []string{metrics.FlushedSegmentLabel, datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), "unknown"}
mixedLabels := []string{metrics.FlushedSegmentLabel, datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), segmentMetricFormatMixed}
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(unknownLabels...)))
err = meta.UpdateSegmentsInfo(context.TODO(), AddBinlogsOperator(1,
[]*datapb.FieldBinlog{
{
FieldID: 100,
ChildFields: []int64{100},
Format: "parquet",
Binlogs: []*datapb.Binlog{
{LogID: 10, EntriesNum: 100, LogSize: 1000, MemorySize: 1000},
},
},
{
FieldID: 101,
ChildFields: []int64{101},
Format: "vortex",
Binlogs: []*datapb.Binlog{
{LogID: 11, EntriesNum: 100, LogSize: 1000, MemorySize: 1000},
},
},
},
nil,
nil,
nil,
))
require.NoError(t, err)
assert.Equal(t, float64(0), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(unknownLabels...)))
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(mixedLabels...)))
}
func TestUpdateSegmentsInfoUpdatesSegmentFormatMetricWithStateChange(t *testing.T) {
metrics.DataCoordNumSegments.Reset()
meta, err := newMemoryMeta(t)
require.NoError(t, err)
defer metrics.DataCoordNumSegments.Reset()
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Growing,
Level: datapb.SegmentLevel_L1,
StorageVersion: storage.StorageV3,
})
require.NoError(t, meta.AddSegment(context.TODO(), segment))
growingUnknownLabels := []string{commonpb.SegmentState_Growing.String(), datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), segmentMetricFormatUnknown}
flushedUnknownLabels := []string{metrics.FlushedSegmentLabel, datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), segmentMetricFormatUnknown}
flushedLanceLabels := []string{metrics.FlushedSegmentLabel, datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), "lance-table"}
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(growingUnknownLabels...)))
err = meta.UpdateSegmentsInfo(context.TODO(),
UpdateStatusOperator(1, commonpb.SegmentState_Flushed),
AddBinlogsOperator(1,
[]*datapb.FieldBinlog{
{
FieldID: 100,
ChildFields: []int64{100},
Format: "lance-table",
Binlogs: []*datapb.Binlog{
{LogID: 10, EntriesNum: 100, LogSize: 1000, MemorySize: 1000},
},
},
},
nil,
nil,
nil,
),
)
require.NoError(t, err)
assert.Equal(t, float64(0), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(growingUnknownLabels...)))
assert.Equal(t, float64(0), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(flushedUnknownLabels...)))
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(flushedLanceLabels...)))
}
func TestUpdateSegmentsInfoUpdatesSegmentFormatMetricWithBinlogsBeforeStateChange(t *testing.T) {
metrics.DataCoordNumSegments.Reset()
meta, err := newMemoryMeta(t)
require.NoError(t, err)
defer metrics.DataCoordNumSegments.Reset()
segment := NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Importing,
Level: datapb.SegmentLevel_L1,
StorageVersion: storage.StorageV3,
NumOfRows: 100,
})
require.NoError(t, meta.AddSegment(context.TODO(), segment))
importingUnknownLabels := []string{commonpb.SegmentState_Importing.String(), datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), segmentMetricFormatUnknown}
importingParquetLabels := []string{commonpb.SegmentState_Importing.String(), datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), "parquet"}
flushedParquetLabels := []string{metrics.FlushedSegmentLabel, datapb.SegmentLevel_L1.String(), "unsorted", fmt.Sprint(storage.StorageV3), "parquet"}
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(importingUnknownLabels...)))
err = meta.UpdateSegmentsInfo(context.TODO(),
UpdateBinlogsOperator(1,
[]*datapb.FieldBinlog{
{
FieldID: 100,
ChildFields: []int64{100},
Format: "parquet",
Binlogs: []*datapb.Binlog{
{LogID: 10, EntriesNum: 100, LogSize: 1000, MemorySize: 1000},
},
},
},
nil,
nil,
nil,
),
UpdateStatusOperator(1, commonpb.SegmentState_Flushed),
)
require.NoError(t, err)
assert.Equal(t, float64(0), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(importingUnknownLabels...)))
assert.Equal(t, float64(0), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(importingParquetLabels...)))
assert.Equal(t, float64(1), prometheustestutil.ToFloat64(metrics.DataCoordNumSegments.WithLabelValues(flushedParquetLabels...)))
}
func TestUpdateManifestVersion(t *testing.T) {
t.Run("segment not found", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
operator := UpdateManifestVersion(999, 10)
pack := &updateSegmentPack{
meta: meta,
segments: make(map[int64]*SegmentInfo),
}
assert.False(t, operator(pack))
})
t.Run("empty manifest path", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
meta.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
ManifestPath: "",
},
})
operator := UpdateManifestVersion(1, 10)
pack := &updateSegmentPack{
meta: meta,
segments: make(map[int64]*SegmentInfo),
}
assert.False(t, operator(pack))
})
t.Run("invalid manifest path - unmarshal error", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
meta.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
ManifestPath: "not-json",
},
})
operator := UpdateManifestVersion(1, 10)
pack := &updateSegmentPack{
meta: meta,
segments: make(map[int64]*SegmentInfo),
}
assert.False(t, operator(pack))
})
t.Run("same version - no update", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
manifestPath := packed.MarshalManifestPath("/data/segments/1", 10)
meta.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
ManifestPath: manifestPath,
},
})
operator := UpdateManifestVersion(1, 10)
pack := &updateSegmentPack{
meta: meta,
segments: make(map[int64]*SegmentInfo),
}
assert.False(t, operator(pack))
})
t.Run("success - version updated", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
manifestPath := packed.MarshalManifestPath("/data/segments/1", 5)
meta.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
ManifestPath: manifestPath,
},
})
operator := UpdateManifestVersion(1, 10)
pack := &updateSegmentPack{
meta: meta,
segments: make(map[int64]*SegmentInfo),
}
assert.True(t, operator(pack))
// Verify the manifest path was updated
seg := pack.Get(1)
assert.NotNil(t, seg)
basePath, version, err := packed.UnmarshalManifestPath(seg.ManifestPath)
assert.NoError(t, err)
assert.Equal(t, "/data/segments/1", basePath)
assert.Equal(t, int64(10), version)
})
t.Run("success - via UpdateSegmentsInfo", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
manifestPath := packed.MarshalManifestPath("/data/segments/1", 1)
meta.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
ManifestPath: manifestPath,
},
})
err = meta.UpdateSegmentsInfo(
context.TODO(),
UpdateManifestVersion(1, 5),
)
assert.NoError(t, err)
updated := meta.GetHealthySegment(context.TODO(), 1)
assert.NotNil(t, updated)
basePath, version, err := packed.UnmarshalManifestPath(updated.ManifestPath)
assert.NoError(t, err)
assert.Equal(t, "/data/segments/1", basePath)
assert.Equal(t, int64(5), version)
})
t.Run("rollback rejected - currentVer > incomingVer is a no-op", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Current version = 10. A stale broadcast carrying version = 5 must
// not regress the pointer. classifyBackfillSegments pre-checks
// monotonicity at broadcast time, but concurrent compaction may have
// advanced ManifestPath between pre-check and this apply -- the
// operator-level guard is the last line of defense.
manifestPath := packed.MarshalManifestPath("/data/segments/1", 10)
meta.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
ManifestPath: manifestPath,
},
})
operator := UpdateManifestVersion(1, 5)
pack := &updateSegmentPack{
meta: meta,
segments: make(map[int64]*SegmentInfo),
}
assert.False(t, operator(pack))
// Confirm the stored manifest path was not mutated in the pack.
got := pack.Get(1)
_, currentVer, err := packed.UnmarshalManifestPath(got.ManifestPath)
assert.NoError(t, err)
assert.Equal(t, int64(10), currentVer)
})
}
func TestUpdateSegmentColumnGroupsOperator(t *testing.T) {
// Helper: build a segment with two pre-existing column groups, where the
// first group owns top-level fieldID=100 and child_fields=[200,201], and
// the second group owns top-level fieldID=300.
newSegmentWithExistingGroups := func() *SegmentInfo {
return &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1000,
State: commonpb.SegmentState_Flushed,
StorageVersion: storage.StorageV2,
DataVersion: int32(5),
Binlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
ChildFields: []int64{200, 201},
Binlogs: []*datapb.Binlog{{LogID: 1}},
},
{
FieldID: 300,
Binlogs: []*datapb.Binlog{{LogID: 2}},
},
},
},
}
}
t.Run("segment not found returns false", func(t *testing.T) {
m, err := newMemoryMeta(t)
assert.NoError(t, err)
op := UpdateSegmentColumnGroupsOperator(999, map[int64]*datapb.FieldBinlog{
400: {FieldID: 400},
})
pack := &updateSegmentPack{
meta: m,
segments: make(map[int64]*SegmentInfo),
increments: make(map[int64]metastore.BinlogsIncrement),
}
assert.False(t, op(pack))
})
t.Run("append new group bumps DataVersion", func(t *testing.T) {
m, err := newMemoryMeta(t)
assert.NoError(t, err)
seg := newSegmentWithExistingGroups()
m.AddSegment(context.Background(), seg)
op := UpdateSegmentColumnGroupsOperator(1, map[int64]*datapb.FieldBinlog{
400: {FieldID: 400, Binlogs: []*datapb.Binlog{{LogID: 10, EntriesNum: 100}}},
})
pack := &updateSegmentPack{
meta: m,
segments: make(map[int64]*SegmentInfo),
increments: make(map[int64]metastore.BinlogsIncrement),
}
assert.True(t, op(pack))
got := pack.Get(1)
assert.NotNil(t, got)
assert.Equal(t, int32(6), got.DataVersion)
// Two pre-existing + one new.
assert.Len(t, got.Binlogs, 3)
var fids []int64
for _, fb := range got.Binlogs {
fids = append(fids, fb.GetFieldID())
}
assert.ElementsMatch(t, []int64{100, 300, 400}, fids)
// child_fields on 100 untouched because no child collision.
for _, fb := range got.Binlogs {
if fb.GetFieldID() == 100 {
assert.ElementsMatch(t, []int64{200, 201}, fb.GetChildFields())
}
}
_, ok := pack.increments[1]
assert.True(t, ok)
})
t.Run("strips child fields from existing group", func(t *testing.T) {
m, err := newMemoryMeta(t)
assert.NoError(t, err)
seg := newSegmentWithExistingGroups()
m.AddSegment(context.Background(), seg)
// new group 500 owns child 200 which was held by group 100.
op := UpdateSegmentColumnGroupsOperator(1, map[int64]*datapb.FieldBinlog{
500: {FieldID: 500, ChildFields: []int64{200}},
})
pack := &updateSegmentPack{
meta: m,
segments: make(map[int64]*SegmentInfo),
increments: make(map[int64]metastore.BinlogsIncrement),
}
assert.True(t, op(pack))
got := pack.Get(1)
for _, fb := range got.Binlogs {
if fb.GetFieldID() == 100 {
assert.ElementsMatch(t, []int64{201}, fb.GetChildFields(),
"child 200 should have been stripped from old group")
}
}
})
t.Run("replace same fieldID in place", func(t *testing.T) {
m, err := newMemoryMeta(t)
assert.NoError(t, err)
seg := newSegmentWithExistingGroups()
m.AddSegment(context.Background(), seg)
op := UpdateSegmentColumnGroupsOperator(1, map[int64]*datapb.FieldBinlog{
100: {FieldID: 100, Binlogs: []*datapb.Binlog{{LogID: 999, EntriesNum: 7}}},
})
pack := &updateSegmentPack{
meta: m,
segments: make(map[int64]*SegmentInfo),
increments: make(map[int64]metastore.BinlogsIncrement),
}
assert.True(t, op(pack))
got := pack.Get(1)
// 100 replaced, 300 preserved => still 2 groups.
assert.Len(t, got.Binlogs, 2)
for _, fb := range got.Binlogs {
if fb.GetFieldID() == 100 {
assert.Len(t, fb.GetBinlogs(), 1)
assert.Equal(t, int64(999), fb.GetBinlogs()[0].GetLogID())
}
}
})
t.Run("drops empty-children existing group and records DroppedBinlogFieldIDs", func(t *testing.T) {
// Pre-existing single-child group (fieldID=100 owns child 200) whose
// only child is claimed by a new schema bump group (fieldID=200). After
// stripping, group 100's ChildFields is empty -- the operator must
// drop it from segment.Binlogs AND record 100 in DroppedBinlogFieldIDs
// so the catalog removes the orphan etcd KV (without it, listBinlogs'
// prefix scan would resurrect the zombie on restart).
m, err := newMemoryMeta(t)
assert.NoError(t, err)
m.AddSegment(context.Background(), &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1000,
State: commonpb.SegmentState_Flushed,
StorageVersion: storage.StorageV2,
Binlogs: []*datapb.FieldBinlog{
{
FieldID: 100,
ChildFields: []int64{200}, // single child
Binlogs: []*datapb.Binlog{{LogID: 1}},
},
{
FieldID: 300,
ChildFields: []int64{301, 302},
Binlogs: []*datapb.Binlog{{LogID: 2}},
},
},
},
})
op := UpdateSegmentColumnGroupsOperator(1, map[int64]*datapb.FieldBinlog{
200: {FieldID: 200, ChildFields: []int64{200}, Binlogs: []*datapb.Binlog{{LogID: 99}}},
})
pack := &updateSegmentPack{
meta: m,
segments: make(map[int64]*SegmentInfo),
increments: make(map[int64]metastore.BinlogsIncrement),
}
assert.True(t, op(pack))
got := pack.Get(1)
// Group 100 must be gone from in-memory binlogs; 300 (unaffected) plus
// new 200 remain.
assert.Len(t, got.Binlogs, 2)
fids := lo.Map(got.Binlogs, func(fb *datapb.FieldBinlog, _ int) int64 { return fb.GetFieldID() })
assert.ElementsMatch(t, []int64{200, 300}, fids)
// Increment carries the orphan FieldID so AlterSegments can remove
// the persisted KV.
inc, ok := pack.increments[1]
assert.True(t, ok)
assert.ElementsMatch(t, []int64{100}, inc.DroppedBinlogFieldIDs)
})
t.Run("DataVersion monotonic across reruns", func(t *testing.T) {
m, err := newMemoryMeta(t)
assert.NoError(t, err)
seg := newSegmentWithExistingGroups()
m.AddSegment(context.Background(), seg)
err = m.UpdateSegmentsInfo(context.Background(),
UpdateSegmentColumnGroupsOperator(1, map[int64]*datapb.FieldBinlog{
400: {FieldID: 400, Binlogs: []*datapb.Binlog{{LogID: 10}}},
}),
)
assert.NoError(t, err)
err = m.UpdateSegmentsInfo(context.Background(),
UpdateSegmentColumnGroupsOperator(1, map[int64]*datapb.FieldBinlog{
400: {FieldID: 400, Binlogs: []*datapb.Binlog{{LogID: 11}}},
}),
)
assert.NoError(t, err)
got := m.GetHealthySegment(context.Background(), 1)
assert.NotNil(t, got)
// Started at 5, two bumps => 7.
assert.Equal(t, int32(7), got.DataVersion)
})
}
func Test_meta_SetSegmentsCompacting(t *testing.T) {
type fields struct {
client kv.MetaKv
segments *SegmentsInfo
}
type args struct {
segmentID UniqueID
compacting bool
}
tests := []struct {
name string
fields fields
args args
}{
{
"test set segment compacting",
fields{
NewMetaMemoryKV(),
func() *SegmentsInfo {
s := NewSegmentsInfo()
s.SetSegment(1, &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Flushed,
},
isCompacting: false,
})
return s
}(),
},
args{
segmentID: 1,
compacting: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := &meta{
catalog: &datacoord.Catalog{MetaKv: tt.fields.client},
segments: tt.fields.segments,
}
m.SetSegmentsCompacting(context.TODO(), []UniqueID{tt.args.segmentID}, tt.args.compacting)
segment := m.GetHealthySegment(context.TODO(), tt.args.segmentID)
assert.Equal(t, tt.args.compacting, segment.isCompacting)
})
}
}
func Test_meta_GetSegmentsOfCollection(t *testing.T) {
storedSegments := NewSegmentsInfo()
for segID, segment := range map[int64]*SegmentInfo{
1: {
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
State: commonpb.SegmentState_Flushed,
},
},
2: {
SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 1,
State: commonpb.SegmentState_Growing,
},
},
3: {
SegmentInfo: &datapb.SegmentInfo{
ID: 3,
CollectionID: 2,
State: commonpb.SegmentState_Flushed,
},
},
} {
storedSegments.SetSegment(segID, segment)
}
expectedSeg := map[int64]commonpb.SegmentState{1: commonpb.SegmentState_Flushed, 2: commonpb.SegmentState_Growing}
m := &meta{segments: storedSegments}
got := m.GetSegmentsOfCollection(context.TODO(), 1)
assert.Equal(t, len(expectedSeg), len(got))
for _, gotInfo := range got {
expected, ok := expectedSeg[gotInfo.ID]
assert.True(t, ok)
assert.Equal(t, expected, gotInfo.GetState())
}
got = m.GetSegmentsOfCollection(context.TODO(), -1)
assert.Equal(t, 3, len(got))
got = m.GetSegmentsOfCollection(context.TODO(), 10)
assert.Equal(t, 0, len(got))
}
func Test_meta_GetSegmentsWithChannel(t *testing.T) {
storedSegments := NewSegmentsInfo()
for segID, segment := range map[int64]*SegmentInfo{
1: {
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
InsertChannel: "h1",
State: commonpb.SegmentState_Flushed,
},
},
2: {
SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 1,
InsertChannel: "h2",
State: commonpb.SegmentState_Growing,
},
},
3: {
SegmentInfo: &datapb.SegmentInfo{
ID: 3,
CollectionID: 2,
State: commonpb.SegmentState_Flushed,
InsertChannel: "h1",
},
},
} {
storedSegments.SetSegment(segID, segment)
}
m := &meta{segments: storedSegments}
got := m.GetSegmentsByChannel("h1")
assert.Equal(t, 2, len(got))
assert.ElementsMatch(t, []int64{1, 3}, lo.Map(
got,
func(s *SegmentInfo, i int) int64 {
return s.ID
},
))
got = m.GetSegmentsByChannel("h3")
assert.Equal(t, 0, len(got))
got = m.SelectSegments(context.TODO(), WithCollection(1), WithChannel("h1"), SegmentFilterFunc(func(segment *SegmentInfo) bool {
return segment != nil && segment.GetState() == commonpb.SegmentState_Flushed
}))
assert.Equal(t, 1, len(got))
assert.ElementsMatch(t, []int64{1}, lo.Map(
got,
func(s *SegmentInfo, i int) int64 {
return s.ID
},
))
m.segments.DropSegment(3)
_, ok := m.segments.secondaryIndexes.coll2Segments[2]
assert.False(t, ok)
assert.Equal(t, 1, len(m.segments.secondaryIndexes.coll2Segments))
assert.Equal(t, 2, len(m.segments.secondaryIndexes.channel2Segments))
segments, ok := m.segments.secondaryIndexes.channel2Segments["h1"]
assert.True(t, ok)
assert.Equal(t, 1, len(segments))
assert.Equal(t, int64(1), segments[1].ID)
segments, ok = m.segments.secondaryIndexes.channel2Segments["h2"]
assert.True(t, ok)
assert.Equal(t, 1, len(segments))
assert.Equal(t, int64(2), segments[2].ID)
m.segments.DropSegment(2)
segments, ok = m.segments.secondaryIndexes.coll2Segments[1]
assert.True(t, ok)
assert.Equal(t, 1, len(segments))
assert.Equal(t, int64(1), segments[1].ID)
assert.Equal(t, 1, len(m.segments.secondaryIndexes.coll2Segments))
assert.Equal(t, 1, len(m.segments.secondaryIndexes.channel2Segments))
segments, ok = m.segments.secondaryIndexes.channel2Segments["h1"]
assert.True(t, ok)
assert.Equal(t, 1, len(segments))
assert.Equal(t, int64(1), segments[1].ID)
_, ok = m.segments.secondaryIndexes.channel2Segments["h2"]
assert.False(t, ok)
}
func TestMeta_HasSegments(t *testing.T) {
m := &meta{
segments: &SegmentsInfo{
segments: map[UniqueID]*SegmentInfo{
1: {
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
NumOfRows: 100,
},
},
},
},
}
has, err := m.HasSegments([]UniqueID{1})
assert.Equal(t, true, has)
assert.NoError(t, err)
has, err = m.HasSegments([]UniqueID{2})
assert.Equal(t, false, has)
assert.Error(t, err)
}
func TestMeta_GetAllSegments(t *testing.T) {
m := &meta{
segments: &SegmentsInfo{
segments: map[UniqueID]*SegmentInfo{
1: {
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
State: commonpb.SegmentState_Growing,
},
},
2: {
SegmentInfo: &datapb.SegmentInfo{
ID: 2,
State: commonpb.SegmentState_Dropped,
},
},
},
},
}
seg1 := m.GetHealthySegment(context.TODO(), 1)
seg1All := m.GetSegment(context.TODO(), 1)
seg2 := m.GetHealthySegment(context.TODO(), 2)
seg2All := m.GetSegment(context.TODO(), 2)
assert.NotNil(t, seg1)
assert.NotNil(t, seg1All)
assert.Nil(t, seg2)
assert.NotNil(t, seg2All)
}
func TestMeta_isSegmentHealthy_issue17823_panic(t *testing.T) {
var seg *SegmentInfo
assert.False(t, isSegmentHealthy(seg))
}
func equalCollectionInfo(t *testing.T, a *collectionInfo, b *collectionInfo) {
assert.Equal(t, a.ID, b.ID)
assert.Equal(t, a.Partitions, b.Partitions)
assert.Equal(t, a.Schema, b.Schema)
assert.Equal(t, a.Properties, b.Properties)
assert.Equal(t, a.StartPositions, b.StartPositions)
}
func TestChannelCP(t *testing.T) {
mockVChannel := "fake-by-dev-rootcoord-dml-1-testchannelcp-v0"
mockPChannel := "fake-by-dev-rootcoord-dml-1"
pos := &msgpb.MsgPosition{
ChannelName: mockPChannel,
MsgID: []byte{0, 0, 0, 0, 0, 0, 0, 0},
Timestamp: 1000,
}
t.Run("UpdateChannelCheckpoint", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// nil position
err = meta.UpdateChannelCheckpoint(context.TODO(), mockVChannel, nil)
assert.Error(t, err)
err = meta.UpdateChannelCheckpoint(context.TODO(), mockVChannel, pos)
assert.NoError(t, err)
})
t.Run("UpdateChannelCheckpoints", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
assert.Equal(t, 0, len(meta.channelCPs.checkpoints))
err = meta.UpdateChannelCheckpoints(context.TODO(), nil)
assert.NoError(t, err)
assert.Equal(t, 0, len(meta.channelCPs.checkpoints))
err = meta.UpdateChannelCheckpoints(context.TODO(), []*msgpb.MsgPosition{pos, {
ChannelName: "",
}})
assert.NoError(t, err)
assert.Equal(t, 1, len(meta.channelCPs.checkpoints))
})
t.Run("GetChannelCheckpoint", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
position := meta.GetChannelCheckpoint(mockVChannel)
assert.Nil(t, position)
err = meta.UpdateChannelCheckpoint(context.TODO(), mockVChannel, pos)
assert.NoError(t, err)
position = meta.GetChannelCheckpoint(mockVChannel)
assert.NotNil(t, position)
assert.True(t, position.ChannelName == pos.ChannelName)
assert.True(t, position.Timestamp == pos.Timestamp)
})
t.Run("DropChannelCheckpoint", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
err = meta.DropChannelCheckpoint(mockVChannel)
assert.NoError(t, err)
err = meta.UpdateChannelCheckpoint(context.TODO(), mockVChannel, pos)
assert.NoError(t, err)
err = meta.DropChannelCheckpoint(mockVChannel)
assert.NoError(t, err)
})
t.Run("WatchChannelCheckpoint", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
err = meta.UpdateChannelCheckpoint(context.TODO(), mockVChannel, pos)
assert.NoError(t, err)
err = meta.WatchChannelCheckpoint(context.TODO(), mockVChannel, pos.Timestamp-1)
assert.NoError(t, err)
})
t.Run("TruncateChannelByTime", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
flushTs := uint64(2000)
channelName := mockVChannel
// Test case 1: No segments to drop
err = meta.TruncateChannelByTime(context.TODO(), channelName, flushTs)
assert.NoError(t, err)
// Test case 2: Add segments that should be dropped (timestamp <= flushTs)
seg1 := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
PartitionID: 1,
InsertChannel: channelName,
State: commonpb.SegmentState_Flushed,
DmlPosition: &msgpb.MsgPosition{
ChannelName: channelName,
Timestamp: flushTs - 100, // less than flushTs
},
},
}
err = meta.AddSegment(context.TODO(), seg1)
assert.NoError(t, err)
// Test case 3: Add segment that should NOT be dropped (timestamp > flushTs)
seg2 := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 1,
PartitionID: 1,
InsertChannel: channelName,
State: commonpb.SegmentState_Flushed,
DmlPosition: &msgpb.MsgPosition{
ChannelName: channelName,
Timestamp: flushTs + 100, // greater than flushTs
},
},
}
err = meta.AddSegment(context.TODO(), seg2)
assert.NoError(t, err)
// Test case 4: Add segment that is already dropped (should be skipped)
seg3 := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 3,
CollectionID: 1,
PartitionID: 1,
InsertChannel: channelName,
State: commonpb.SegmentState_Dropped,
DmlPosition: &msgpb.MsgPosition{
ChannelName: channelName,
Timestamp: flushTs - 100, // less than flushTs but already dropped
},
},
}
err = meta.AddSegment(context.TODO(), seg3)
assert.NoError(t, err)
// Test case 5: TruncateChannelByTime should drop seg1 but not seg2 or seg3
err = meta.TruncateChannelByTime(context.TODO(), channelName, flushTs)
assert.NoError(t, err)
// Verify seg1 is dropped
seg1After := meta.GetSegment(context.TODO(), seg1.ID)
assert.NotNil(t, seg1After)
assert.Equal(t, commonpb.SegmentState_Dropped, seg1After.GetState())
// Verify seg2 is not dropped
seg2After := meta.GetSegment(context.TODO(), seg2.ID)
assert.NotNil(t, seg2After)
assert.NotEqual(t, commonpb.SegmentState_Dropped, seg2After.GetState())
// Verify seg3 remains dropped
seg3After := meta.GetSegment(context.TODO(), seg3.ID)
assert.NotNil(t, seg3After)
assert.Equal(t, commonpb.SegmentState_Dropped, seg3After.GetState())
// Test case 6: Call again with same flushTs, should return nil (no segments to drop)
err = meta.TruncateChannelByTime(context.TODO(), channelName, flushTs)
assert.NoError(t, err)
})
}
func Test_meta_GcConfirm(t *testing.T) {
m := &meta{}
catalog := mocks2.NewDataCoordCatalog(t)
m.catalog = catalog
catalog.On("GcConfirm",
mock.Anything,
mock.AnythingOfType("int64"),
mock.AnythingOfType("int64")).
Return(false)
assert.False(t, m.GcConfirm(context.TODO(), 100, 10000))
}
func Test_meta_ReloadCollectionsFromRootcoords(t *testing.T) {
t.Run("fail to list database", func(t *testing.T) {
m := &meta{
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
}
mockBroker := broker.NewMockBroker(t)
mockBroker.EXPECT().ListDatabases(mock.Anything).Return(nil, errors.New("list database failed, mocked"))
err := m.reloadCollectionsFromRootcoord(context.TODO(), mockBroker)
assert.Error(t, err)
})
t.Run("fail to show collections", func(t *testing.T) {
m := &meta{
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
}
mockBroker := broker.NewMockBroker(t)
mockBroker.EXPECT().ListDatabases(mock.Anything).Return(&milvuspb.ListDatabasesResponse{
DbNames: []string{"db1"},
}, nil)
mockBroker.EXPECT().ShowCollections(mock.Anything, mock.Anything).Return(nil, errors.New("show collections failed, mocked"))
err := m.reloadCollectionsFromRootcoord(context.TODO(), mockBroker)
assert.Error(t, err)
})
t.Run("fail to describe collection", func(t *testing.T) {
m := &meta{
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
}
mockBroker := broker.NewMockBroker(t)
mockBroker.EXPECT().ListDatabases(mock.Anything).Return(&milvuspb.ListDatabasesResponse{
DbNames: []string{"db1"},
}, nil)
mockBroker.EXPECT().ShowCollections(mock.Anything, mock.Anything).Return(&milvuspb.ShowCollectionsResponse{
CollectionNames: []string{"coll1"},
CollectionIds: []int64{1000},
}, nil)
mockBroker.EXPECT().DescribeCollectionInternal(mock.Anything, mock.Anything).Return(nil, errors.New("describe collection failed, mocked"))
err := m.reloadCollectionsFromRootcoord(context.TODO(), mockBroker)
assert.Error(t, err)
})
t.Run("fail to show partitions", func(t *testing.T) {
m := &meta{
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
}
mockBroker := broker.NewMockBroker(t)
mockBroker.EXPECT().ListDatabases(mock.Anything).Return(&milvuspb.ListDatabasesResponse{
DbNames: []string{"db1"},
}, nil)
mockBroker.EXPECT().ShowCollections(mock.Anything, mock.Anything).Return(&milvuspb.ShowCollectionsResponse{
CollectionNames: []string{"coll1"},
CollectionIds: []int64{1000},
}, nil)
mockBroker.EXPECT().DescribeCollectionInternal(mock.Anything, mock.Anything).Return(&milvuspb.DescribeCollectionResponse{}, nil)
mockBroker.EXPECT().ShowPartitionsInternal(mock.Anything, mock.Anything).Return(nil, errors.New("show partitions failed, mocked"))
err := m.reloadCollectionsFromRootcoord(context.TODO(), mockBroker)
assert.Error(t, err)
})
t.Run("success", func(t *testing.T) {
m := &meta{
collections: typeutil.NewConcurrentMap[UniqueID, *collectionInfo](),
}
mockBroker := broker.NewMockBroker(t)
mockBroker.EXPECT().ListDatabases(mock.Anything).Return(&milvuspb.ListDatabasesResponse{
DbNames: []string{"db1"},
}, nil)
mockBroker.EXPECT().ShowCollections(mock.Anything, mock.Anything).Return(&milvuspb.ShowCollectionsResponse{
CollectionNames: []string{"coll1"},
CollectionIds: []int64{1000},
}, nil)
mockBroker.EXPECT().DescribeCollectionInternal(mock.Anything, mock.Anything).Return(&milvuspb.DescribeCollectionResponse{
CollectionID: 1000,
}, nil)
mockBroker.EXPECT().ShowPartitionsInternal(mock.Anything, mock.Anything).Return([]int64{2000}, nil)
err := m.reloadCollectionsFromRootcoord(context.TODO(), mockBroker)
assert.NoError(t, err)
c := m.GetCollection(UniqueID(1000))
assert.NotNil(t, c)
})
}
func TestMeta_GetSegmentsJSON(t *testing.T) {
// Create a mock meta object
m := &meta{
segments: &SegmentsInfo{
segments: map[int64]*SegmentInfo{
1: {
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
PartitionID: 1,
InsertChannel: "channel1",
NumOfRows: 100,
State: commonpb.SegmentState_Growing,
MaxRowNum: 1000,
Compacted: false,
},
},
2: {
SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 2,
PartitionID: 2,
InsertChannel: "channel2",
NumOfRows: 200,
State: commonpb.SegmentState_Sealed,
MaxRowNum: 2000,
Compacted: true,
},
},
},
},
}
segments := m.getSegmentsMetrics(0)
// Check the length of the segments
assert.Equal(t, 2, len(segments))
slices.SortFunc(segments, func(i, j *metricsinfo.Segment) int { return int(i.SegmentID - j.SegmentID) })
// Check the first segment
assert.Equal(t, int64(1), segments[0].SegmentID)
assert.Equal(t, int64(1), segments[0].CollectionID)
assert.Equal(t, int64(1), segments[0].PartitionID)
assert.Equal(t, "channel1", segments[0].Channel)
assert.Equal(t, int64(100), segments[0].NumOfRows)
assert.Equal(t, "Growing", segments[0].State)
assert.False(t, segments[0].Compacted)
// Check the second segment
assert.Equal(t, int64(2), segments[1].SegmentID)
assert.Equal(t, int64(2), segments[1].CollectionID)
assert.Equal(t, int64(2), segments[1].PartitionID)
assert.Equal(t, "channel2", segments[1].Channel)
assert.Equal(t, int64(200), segments[1].NumOfRows)
assert.Equal(t, "Sealed", segments[1].State)
assert.True(t, segments[1].Compacted)
}
func TestTruncateChannelByTime(t *testing.T) {
t.Run("import segment not dropped when flushTs < commit_timestamp", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
err = meta.AddSegment(context.Background(), &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 100,
InsertChannel: "ch1",
State: commonpb.SegmentState_Flushed,
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch1", Timestamp: 1000},
CommitTimestamp: 5000,
}})
assert.NoError(t, err)
err = meta.TruncateChannelByTime(context.Background(), "ch1", 3000)
assert.NoError(t, err)
seg := meta.GetSegment(context.TODO(), 100)
assert.NotEqual(t, commonpb.SegmentState_Dropped, seg.GetState(),
"import segment should NOT be dropped when flushTs < commit_timestamp")
})
t.Run("import segment dropped when flushTs >= commit_timestamp", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
err = meta.AddSegment(context.Background(), &SegmentInfo{SegmentInfo: &datapb.SegmentInfo{
ID: 101,
InsertChannel: "ch2",
State: commonpb.SegmentState_Flushed,
DmlPosition: &msgpb.MsgPosition{ChannelName: "ch2", Timestamp: 1000},
CommitTimestamp: 5000,
}})
assert.NoError(t, err)
err = meta.TruncateChannelByTime(context.Background(), "ch2", 6000)
assert.NoError(t, err)
seg := meta.GetSegment(context.TODO(), 101)
assert.Equal(t, commonpb.SegmentState_Dropped, seg.GetState(),
"import segment should be dropped when flushTs >= commit_timestamp")
})
}
func Test_meta_DropSegmentsOfPartition(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
err = meta.AddSegment(context.Background(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 1,
PartitionID: 1,
CollectionID: 1,
}))
assert.NoError(t, err)
err = meta.AddSegment(context.Background(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 2,
PartitionID: 1,
CollectionID: 1,
}))
assert.NoError(t, err)
err = meta.AddSegment(context.Background(), NewSegmentInfo(&datapb.SegmentInfo{
ID: 3,
PartitionID: 2,
CollectionID: 1,
}))
assert.NoError(t, err)
err = meta.DropSegmentsOfPartition(context.Background(), []int64{1})
assert.NoError(t, err)
segment := meta.GetSegment(context.Background(), 1)
assert.Equal(t, commonpb.SegmentState_Dropped, segment.GetState())
segment = meta.GetSegment(context.Background(), 2)
assert.Equal(t, commonpb.SegmentState_Dropped, segment.GetState())
segment = meta.GetSegment(context.Background(), 3)
assert.NotEqual(t, commonpb.SegmentState_Dropped, segment.GetState())
}
func TestGetMinGrowingSegmentCheckpoint(t *testing.T) {
mockVChannel := "fake-by-dev-rootcoord-dml-1-testgrowing-v0"
t.Run("empty returns nil", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
pos := meta.GetMinGrowingSegmentCheckpoint(mockVChannel)
assert.Nil(t, pos)
})
t.Run("growing L1 segments return min DmlPosition", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Register a TEXT collection so the checkpoint logic applies
meta.collections.Insert(1, &collectionInfo{
ID: 1,
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64},
{FieldID: 101, Name: "text_field", DataType: schemapb.DataType_Text},
},
},
})
// Growing L1 segment with T300
seg1 := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
PartitionID: 1,
InsertChannel: mockVChannel,
State: commonpb.SegmentState_Growing,
Level: datapb.SegmentLevel_L1,
DmlPosition: &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{1},
Timestamp: 300,
},
},
}
err = meta.AddSegment(context.TODO(), seg1)
assert.NoError(t, err)
// Growing L1 segment with T500
seg2 := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 2,
CollectionID: 1,
PartitionID: 1,
InsertChannel: mockVChannel,
State: commonpb.SegmentState_Growing,
Level: datapb.SegmentLevel_L1,
DmlPosition: &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{2},
Timestamp: 500,
},
},
}
err = meta.AddSegment(context.TODO(), seg2)
assert.NoError(t, err)
pos := meta.GetMinGrowingSegmentCheckpoint(mockVChannel)
assert.NotNil(t, pos)
assert.Equal(t, uint64(300), pos.GetTimestamp())
})
t.Run("L0 segments excluded", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Growing L0 segment with T100
seg := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
PartitionID: 1,
InsertChannel: mockVChannel,
State: commonpb.SegmentState_Growing,
Level: datapb.SegmentLevel_L0,
DmlPosition: &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{1},
Timestamp: 100,
},
},
}
err = meta.AddSegment(context.TODO(), seg)
assert.NoError(t, err)
pos := meta.GetMinGrowingSegmentCheckpoint(mockVChannel)
assert.Nil(t, pos)
})
t.Run("non-Growing segments excluded", func(t *testing.T) {
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Flushed L1 segment with T100
seg := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
PartitionID: 1,
InsertChannel: mockVChannel,
State: commonpb.SegmentState_Flushed,
Level: datapb.SegmentLevel_L1,
DmlPosition: &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{1},
Timestamp: 100,
},
},
}
err = meta.AddSegment(context.TODO(), seg)
assert.NoError(t, err)
pos := meta.GetMinGrowingSegmentCheckpoint(mockVChannel)
assert.Nil(t, pos)
})
}
func TestUpdateChannelCheckpoint_ClampedByGrowing(t *testing.T) {
mockVChannel := "fake-by-dev-rootcoord-dml-1-testclamp-v0"
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Register a TEXT collection so clamping is enabled
meta.collections.Insert(1, &collectionInfo{
ID: 1,
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64},
{FieldID: 101, Name: "text_field", DataType: schemapb.DataType_Text},
},
},
})
// Add a growing L1 segment at T300
seg := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
PartitionID: 1,
InsertChannel: mockVChannel,
State: commonpb.SegmentState_Growing,
Level: datapb.SegmentLevel_L1,
DmlPosition: &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{1},
Timestamp: 300,
},
},
}
err = meta.AddSegment(context.TODO(), seg)
assert.NoError(t, err)
// Update checkpoint with T500, should be clamped to T300 (TEXT collection)
err = meta.UpdateChannelCheckpoint(context.TODO(), mockVChannel, &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{0, 0, 0, 0, 0, 0, 0, 0},
Timestamp: 500,
})
assert.NoError(t, err)
cp := meta.GetChannelCheckpoint(mockVChannel)
assert.NotNil(t, cp)
assert.Equal(t, uint64(300), cp.GetTimestamp())
}
func TestUpdateChannelCheckpoint_NotClampedForNonTextCollection(t *testing.T) {
mockVChannel := "fake-by-dev-rootcoord-dml-1-testclamp-notext-v0"
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Register a non-TEXT collection — clamping should NOT apply
meta.collections.Insert(2, &collectionInfo{
ID: 2,
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64},
{FieldID: 101, Name: "varchar_field", DataType: schemapb.DataType_VarChar},
},
},
})
// Add a growing L1 segment at T300 for the non-TEXT collection
seg := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 10,
CollectionID: 2,
PartitionID: 1,
InsertChannel: mockVChannel,
State: commonpb.SegmentState_Growing,
Level: datapb.SegmentLevel_L1,
DmlPosition: &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{1},
Timestamp: 300,
},
},
}
err = meta.AddSegment(context.TODO(), seg)
assert.NoError(t, err)
// Update checkpoint with T500 — should NOT be clamped (non-TEXT collection)
err = meta.UpdateChannelCheckpoint(context.TODO(), mockVChannel, &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{0, 0, 0, 0, 0, 0, 0, 0},
Timestamp: 500,
})
assert.NoError(t, err)
cp := meta.GetChannelCheckpoint(mockVChannel)
assert.NotNil(t, cp)
assert.Equal(t, uint64(500), cp.GetTimestamp()) // NOT clamped
}
func TestUpdateChannelCheckpoints_ClampedByGrowing(t *testing.T) {
mockVChannel := "fake-by-dev-rootcoord-dml-1-testclampbatch-v0"
meta, err := newMemoryMeta(t)
assert.NoError(t, err)
// Register a TEXT collection so clamping is enabled
meta.collections.Insert(1, &collectionInfo{
ID: 1,
Schema: &schemapb.CollectionSchema{
Fields: []*schemapb.FieldSchema{
{FieldID: 100, Name: "pk", DataType: schemapb.DataType_Int64},
{FieldID: 101, Name: "text_field", DataType: schemapb.DataType_Text},
},
},
})
// Add a growing L1 segment at T300
seg := &SegmentInfo{
SegmentInfo: &datapb.SegmentInfo{
ID: 1,
CollectionID: 1,
PartitionID: 1,
InsertChannel: mockVChannel,
State: commonpb.SegmentState_Growing,
Level: datapb.SegmentLevel_L1,
DmlPosition: &msgpb.MsgPosition{
ChannelName: mockVChannel,
MsgID: []byte{1},
Timestamp: 300,
},
},
}
err = meta.AddSegment(context.TODO(), seg)
assert.NoError(t, err)
// Batch update checkpoint with T500, should be clamped to T300 (TEXT collection)
err = meta.UpdateChannelCheckpoints(context.TODO(), []*msgpb.MsgPosition{
{
ChannelName: mockVChannel,
MsgID: []byte{0, 0, 0, 0, 0, 0, 0, 0},
Timestamp: 500,
},
})
assert.NoError(t, err)
cp := meta.GetChannelCheckpoint(mockVChannel)
assert.NotNil(t, cp)
assert.Equal(t, uint64(300), cp.GetTimestamp())
}