You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@skywalking.apache.org by ha...@apache.org on 2026/04/30 05:11:04 UTC
(skywalking-banyandb) branch v0.10.x updated (dda5b5edf -> 55c6007c2)
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a change to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
from dda5b5edf chore(version): Cherry-pick bug fixes from main for v0.10.1
new f822dcdc1 Fix the reuse of byte arrays in the underlying min/max implementation (#1022)
new a14b7d486 Fix property schema client connection not stable after data node restarted (#1038)
new b21e06f33 fix take snapshot error when no data in the segment (#1046)
new d01e715ef fix(storage): disable rotation task on warm and cold lifecycle nodes (#1055)
new 256835ca6 fix(storage): prevent epoch segment creation from zero timestamps (#1059)
new cd3ac57e3 chore(deps): upgrade Go and npm dependencies (#1058)
new 9634d8268 fix(sidx): use MinTimestamp/MaxTimestamp instead of SegmentID in streaming sync (#1060)
new a8a083e44 fix(handoff): prevent size limit bypass and sidx timestamp corruption in handoff replay (#1064)
new 6304de7b9 fix(handoff): prevent enqueuing parts for online nodes via shared LocateAll (#1065)
new 1624c5903 Fix wrong backup path of schema property (#1070)
new ab07c3f93 Fix OOM issue cause during migration when a group contains a large amount of data (#1072)
new d1ee661b3 Fix lifecycle migration failure when the target stage has `close: true` (#1077)
new ca6a0d352 Fix stale sync request blocking watch session channel (#1080)
new 37dcb4f3c Fix nil pointer panic in disk monitor during early initialization (#1082)
new e11effa4d Fix FileSystemError not matching io/fs.ErrNotExist sentinel (#1088)
new af4c71362 fix(topn): deduplicate entities in TopN aggregation query (#1085)
new 081464e30 fix(stream): skip element-index visit when idx/ is absent (#1090)
new a712c2ef0 fix(metadata): widen FODC inspection broadcast deadline and parallelize InspectAll (#1092)
new 74d9f7409 fix(measure,stream,trace): eliminate flusher/introducer data race on shutdown (#1094)
new 85d14e6d3 fix: Use `topic` instead of `session_id` as the Prometheus label (#1093)
new 4178a4983 fix(fodc): heal reconnect deadlock and add error message when no agents for lifecycle request (#1101)
new c31232e23 chore: bump ui and mcp npm dependencies (#1104)
new 15259e5f4 chore(version): Cherry-pick bug fixes from main for v0.10.2
new 55c6007c2 chore: update CHANGES.md and sync .gitignore from main
The 24 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.gitignore | 8 +-
CHANGES.md | 32 +-
banyand/backup/backup_test.go | 9 +-
banyand/backup/lifecycle/steps.go | 3 +-
banyand/backup/snapshot/snapshot.go | 10 +-
banyand/internal/sidx/block_writer.go | 31 +-
banyand/internal/sidx/file_part_sync_test.go | 134 +++
banyand/internal/sidx/interfaces.go | 2 +
banyand/internal/sidx/metadata.go | 19 +
banyand/internal/sidx/part.go | 62 +-
banyand/internal/sidx/sidx.go | 14 +
banyand/internal/sidx/sync.go | 19 +-
banyand/internal/sidx/sync_test.go | 191 ++++
banyand/internal/storage/disk_monitor.go | 27 +-
banyand/internal/storage/disk_monitor_test.go | 41 +-
banyand/internal/storage/rotation.go | 5 +-
banyand/internal/storage/rotation_test.go | 129 +++
banyand/internal/storage/segment.go | 12 +
banyand/internal/storage/storage.go | 4 +-
banyand/internal/storage/tsdb.go | 34 +-
banyand/internal/storage/tsdb_test.go | 148 +++
banyand/liaison/grpc/node.go | 29 +
banyand/liaison/grpc/node_test.go | 11 +
banyand/measure/block_writer.go | 26 +-
banyand/measure/file_part_sync_test.go | 207 ++++
banyand/measure/flusher.go | 5 +-
banyand/measure/introducer.go | 59 +-
banyand/measure/merger.go | 6 +-
banyand/measure/metadata.go | 20 +-
banyand/measure/part_metadata.go | 9 +
banyand/measure/snapshot.go | 13 +-
banyand/measure/snapshot_test.go | 40 +
banyand/measure/syncer.go | 6 +-
banyand/measure/tstable.go | 37 +-
banyand/measure/write_data.go | 98 +-
banyand/measure/write_data_segmentref_test.go | 250 +++++
banyand/measure/write_index.go | 5 +
banyand/metadata/client.go | 96 +-
banyand/metadata/schema/collector.go | 10 +-
banyand/metadata/schema/property/client.go | 195 +++-
banyand/metadata/schema/property/client_test.go | 118 ++-
banyand/query/processor_topn.go | 42 +-
banyand/queue/pub/pub.go | 2 +-
banyand/queue/sub/chunked_sync.go | 16 +-
banyand/queue/sub/chunked_sync_test.go | 129 +++
banyand/queue/sub/group_lifecycle.go | 35 +-
banyand/queue/sub/group_lifecycle_test.go | 57 +
banyand/queue/sub/server.go | 26 +-
banyand/stream/block_writer.go | 29 +-
banyand/stream/file_part_sync_test.go | 195 ++++
banyand/stream/flusher.go | 5 +-
banyand/stream/introducer.go | 60 +-
banyand/stream/merger.go | 6 +-
banyand/stream/metadata.go | 20 +-
banyand/stream/part_metadata.go | 9 +
banyand/stream/snapshot.go | 24 +-
banyand/stream/snapshot_test.go | 41 +
banyand/stream/syncer.go | 6 +-
banyand/stream/tag_filter_test.go | 44 +
banyand/stream/tag_metadata.go | 8 +
banyand/stream/tstable.go | 37 +-
banyand/stream/visitor.go | 17 +-
banyand/stream/visitor_test.go | 34 +
banyand/stream/write_data.go | 92 +-
banyand/stream/write_data_segmentref_test.go | 248 +++++
banyand/stream/write_index.go | 9 +
banyand/trace/block_writer.go | 26 +-
banyand/trace/file_part_sync_test.go | 549 ++++++++++
banyand/trace/flusher.go | 5 +-
banyand/trace/handoff_controller.go | 123 ++-
banyand/trace/handoff_replay_test.go | 332 ++++++
banyand/trace/handoff_storage_test.go | 120 +++
banyand/trace/introducer.go | 80 +-
banyand/trace/merger.go | 6 +-
banyand/trace/metadata.go | 20 +-
banyand/trace/migration_test.go | 1106 ++++++++++++++++++++
banyand/trace/part_metadata.go | 9 +
banyand/trace/snapshot.go | 26 +-
banyand/trace/snapshot_test.go | 46 +
banyand/trace/streaming_pipeline_test.go | 8 +
banyand/trace/svc_liaison.go | 18 +-
banyand/trace/syncer.go | 6 +-
banyand/trace/tstable.go | 38 +-
banyand/trace/write_data.go | 155 ++-
banyand/trace/write_data_segmentref_test.go | 354 +++++++
banyand/trace/write_index.go | 5 +
dist/LICENSE | 176 ++--
.../mcp-licenses/license-eventsource-parser.txt | 2 +-
dist/licenses/ui-licenses/license-postcss.txt | 2 +-
docs/concept/rotation.md | 9 +
docs/operation/configuration.md | 11 +
docs/operation/lifecycle.md | 9 +
fodc/agent/internal/proxy/client.go | 65 +-
.../internal/proxy/client_blocking_send_test.go | 151 +++
fodc/agent/internal/proxy/client_test.go | 13 +
fodc/agent/internal/proxy/wait.go | 69 ++
fodc/agent/internal/proxy/wait_test.go | 61 ++
fodc/proxy/internal/api/server.go | 30 +-
fodc/proxy/internal/api/server_test.go | 52 +
fodc/proxy/internal/lifecycle/manager.go | 112 +-
fodc/proxy/internal/lifecycle/manager_test.go | 31 +-
go.mod | 116 +-
go.sum | 264 +++--
mcp/LICENSE | 26 +-
mcp/package-lock.json | 538 +++++-----
pkg/fs/error.go | 22 +-
pkg/fs/error_test.go | 63 ++
pkg/grpchelper/connmanager.go | 74 +-
pkg/grpchelper/connmanager_test.go | 208 ++++
pkg/grpchelper/helpers_test.go | 21 +
pkg/query/logical/measure/topn_analyzer.go | 18 +-
pkg/query/logical/measure/topn_plan_distinct.go | 268 +++++
.../logical/measure/topn_plan_distinct_test.go | 173 +++
...json => service_instance_metric_topn_test.json} | 14 +-
...nute_top_bottom_100.json => topn_agg_test.json} | 6 +-
test/cases/init.go | 1 +
...=> service_instance_metric_topn_test_data.json} | 30 +-
test/cases/topn/data/data.go | 91 ++
.../cases/topn/data/input/{ne.ql => topn_count.ql} | 6 +-
.../topn/data/input/{in.yaml => topn_count.yaml} | 6 +-
test/cases/topn/data/input/{ne.ql => topn_max.ql} | 4 +-
.../topn/data/input/{in.yaml => topn_max.yaml} | 4 +-
test/cases/topn/data/input/{eq.ql => topn_mean.ql} | 4 +-
.../topn/data/input/{in.yaml => topn_mean.yaml} | 4 +-
.../input/all.ql => topn/data/input/topn_min.ql} | 6 +-
.../topn/data/input/{in.yaml => topn_min.yaml} | 8 +-
test/cases/topn/data/input/{eq.ql => topn_sum.ql} | 6 +-
.../topn/data/input/{in.yaml => topn_sum.yaml} | 6 +-
.../topn/data/want/{ne.yaml => topn_count.yaml} | 19 +-
.../topn/data/want/{not_in.yaml => topn_max.yaml} | 15 +-
.../{condition_aggr_desc.yaml => topn_mean.yaml} | 12 +-
.../topn/data/want/{not_in.yaml => topn_min.yaml} | 17 +-
.../topn/data/want/{not_in.yaml => topn_sum.yaml} | 15 +-
test/cases/topn/topn.go | 16 +-
test/integration/handoff/handoff_suite_test.go | 44 +
ui/LICENSE | 34 +-
ui/package-lock.json | 429 ++++----
137 files changed, 8219 insertions(+), 1559 deletions(-)
create mode 100644 banyand/internal/sidx/file_part_sync_test.go
create mode 100644 banyand/internal/sidx/sync_test.go
create mode 100644 banyand/measure/file_part_sync_test.go
create mode 100644 banyand/measure/write_data_segmentref_test.go
create mode 100644 banyand/stream/file_part_sync_test.go
create mode 100644 banyand/stream/write_data_segmentref_test.go
create mode 100644 banyand/trace/file_part_sync_test.go
create mode 100644 banyand/trace/migration_test.go
create mode 100644 banyand/trace/write_data_segmentref_test.go
create mode 100644 fodc/agent/internal/proxy/client_blocking_send_test.go
create mode 100644 fodc/agent/internal/proxy/wait.go
create mode 100644 fodc/agent/internal/proxy/wait_test.go
create mode 100644 pkg/fs/error_test.go
create mode 100644 pkg/query/logical/measure/topn_plan_distinct.go
create mode 100644 pkg/query/logical/measure/topn_plan_distinct_test.go
copy pkg/test/measure/testdata/measures/{endpoint_resp_time_minute.json => service_instance_metric_topn_test.json} (72%)
copy pkg/test/measure/testdata/topn_aggregations/{service_instance_cpm_minute_top_bottom_100.json => topn_agg_test.json} (70%)
copy test/cases/measure/data/testdata/{service_instance_latency_minute_data.json => service_instance_metric_topn_test_data.json} (83%)
copy test/cases/topn/data/input/{ne.ql => topn_count.ql} (92%)
copy test/cases/topn/data/input/{in.yaml => topn_count.yaml} (95%)
copy test/cases/topn/data/input/{ne.ql => topn_max.ql} (94%)
copy test/cases/topn/data/input/{in.yaml => topn_max.yaml} (96%)
copy test/cases/topn/data/input/{eq.ql => topn_mean.ql} (93%)
copy test/cases/topn/data/input/{in.yaml => topn_mean.yaml} (96%)
copy test/cases/{measure/data/input/all.ql => topn/data/input/topn_min.ql} (91%)
copy test/cases/topn/data/input/{in.yaml => topn_min.yaml} (93%)
copy test/cases/topn/data/input/{eq.ql => topn_sum.ql} (92%)
copy test/cases/topn/data/input/{in.yaml => topn_sum.yaml} (95%)
copy test/cases/topn/data/want/{ne.yaml => topn_count.yaml} (85%)
copy test/cases/topn/data/want/{not_in.yaml => topn_max.yaml} (87%)
copy test/cases/topn/data/want/{condition_aggr_desc.yaml => topn_mean.yaml} (88%)
copy test/cases/topn/data/want/{not_in.yaml => topn_min.yaml} (85%)
copy test/cases/topn/data/want/{not_in.yaml => topn_sum.yaml} (87%)
(skywalking-banyandb) 11/24: Fix OOM issue cause during migration when a group contains a large amount of data (#1072)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit ab07c3f93e4170355419880f76592d834f6659ec
Author: mrproliu <74...@qq.com>
AuthorDate: Fri Apr 17 10:11:11 2026 +0800
Fix OOM issue cause during migration when a group contains a large amount of data (#1072)
---
banyand/internal/sidx/block_writer.go | 31 +-
banyand/internal/sidx/file_part_sync_test.go | 134 ++++
banyand/internal/sidx/interfaces.go | 2 +
banyand/internal/sidx/metadata.go | 19 +
banyand/internal/sidx/part.go | 62 +-
banyand/internal/sidx/sidx.go | 14 +
banyand/measure/block_writer.go | 26 +-
banyand/measure/file_part_sync_test.go | 207 +++++
banyand/measure/introducer.go | 20 +-
banyand/measure/part_metadata.go | 9 +
banyand/measure/tstable.go | 27 +-
banyand/measure/write_data.go | 82 +-
banyand/stream/block_writer.go | 29 +-
banyand/stream/file_part_sync_test.go | 195 +++++
banyand/stream/introducer.go | 21 +-
banyand/stream/part_metadata.go | 9 +
banyand/stream/tstable.go | 27 +-
banyand/stream/write_data.go | 73 +-
banyand/trace/block_writer.go | 26 +-
banyand/trace/file_part_sync_test.go | 549 +++++++++++++
banyand/trace/introducer.go | 41 +-
banyand/trace/migration_test.go | 1106 ++++++++++++++++++++++++++
banyand/trace/part_metadata.go | 9 +
banyand/trace/streaming_pipeline_test.go | 8 +
banyand/trace/tstable.go | 28 +-
banyand/trace/write_data.go | 127 +--
26 files changed, 2654 insertions(+), 227 deletions(-)
diff --git a/banyand/internal/sidx/block_writer.go b/banyand/internal/sidx/block_writer.go
index 75645b4fd..607844eea 100644
--- a/banyand/internal/sidx/block_writer.go
+++ b/banyand/internal/sidx/block_writer.go
@@ -116,6 +116,21 @@ func (sw *writers) MustInitForMemPart(mp *memPart) {
sw.keysWriter.init(&mp.keys)
}
+// MustInitForFilePart initializes writers for file-backed part.
+func (sw *writers) MustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool) {
+ sw.reset()
+ fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
+ sw.mustCreateTagWriters = func(name string) (fs.Writer, fs.Writer, fs.Writer) {
+ return fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagMetadataExtension), storage.FilePerm, shouldCache),
+ fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagDataExtension), storage.FilePerm, shouldCache),
+ fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagFilterExtension), storage.FilePerm, shouldCache)
+ }
+ sw.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
+ sw.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
+ sw.dataWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, dataFilename), storage.FilePerm, shouldCache))
+ sw.keysWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, keysFilename), storage.FilePerm, shouldCache))
+}
+
// totalBytesWritten returns total bytes written across all writers.
func (sw *writers) totalBytesWritten() uint64 {
n := sw.metaWriter.bytesWritten + sw.primaryWriter.bytesWritten +
@@ -244,21 +259,7 @@ func (bw *blockWriter) MustInitForMemPart(mp *memPart) {
// mustInitForFilePart initializes block writer for file part.
func (bw *blockWriter) mustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool) {
bw.reset()
- fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
-
- bw.writers.mustCreateTagWriters = func(name string) (fs.Writer, fs.Writer, fs.Writer) {
- metaPath := filepath.Join(path, name+tagMetadataExtension)
- dataPath := filepath.Join(path, name+tagDataExtension)
- filterPath := filepath.Join(path, name+tagFilterExtension)
- return fs.MustCreateFile(fileSystem, metaPath, storage.FilePerm, shouldCache),
- fs.MustCreateFile(fileSystem, dataPath, storage.FilePerm, shouldCache),
- fs.MustCreateFile(fileSystem, filterPath, storage.FilePerm, shouldCache)
- }
-
- bw.writers.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
- bw.writers.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
- bw.writers.dataWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, dataFilename), storage.FilePerm, shouldCache))
- bw.writers.keysWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, keysFilename), storage.FilePerm, shouldCache))
+ bw.writers.MustInitForFilePart(fileSystem, path, shouldCache)
}
// MustWriteElements writes elements to the block writer.
diff --git a/banyand/internal/sidx/file_part_sync_test.go b/banyand/internal/sidx/file_part_sync_test.go
new file mode 100644
index 000000000..890a4dce6
--- /dev/null
+++ b/banyand/internal/sidx/file_part_sync_test.go
@@ -0,0 +1,134 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 sidx
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ "github.com/apache/skywalking-banyandb/banyand/observability"
+ "github.com/apache/skywalking-banyandb/banyand/protector"
+ "github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/test/flags"
+)
+
+// TestSyncPartContext_SetForFile_WritesFiles verifies that SetForFile creates the expected
+// files on disk when data is written through the writers and then flushed.
+func TestSyncPartContext_SetForFile_WritesFiles(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ destPath := filepath.Join(tmpPath, "sidx-part-1")
+
+ spc := NewSyncPartContext()
+ spc.SetForFile("test-part", fileSystem, destPath, &queue.ChunkedSyncPartContext{}, false)
+
+ w := spc.GetWriters()
+ require.NotNil(t, w, "writers must be initialized after SetForFile")
+
+ // Write some minimal data to the core writers to produce non-empty files.
+ w.SidxMetaWriter().MustWrite([]byte("meta-data"))
+ w.SidxPrimaryWriter().MustWrite([]byte("primary-data"))
+ w.SidxDataWriter().MustWrite([]byte("data-content"))
+ w.SidxKeysWriter().MustWrite([]byte("keys-content"))
+
+ finishedPath := spc.Finish()
+ require.Equal(t, destPath, finishedPath, "Finish should return the part path")
+
+ // All core files must exist on disk.
+ for _, name := range []string{metaFilename, primaryFilename, dataFilename, keysFilename} {
+ _, err := os.Stat(filepath.Join(destPath, name))
+ require.NoError(t, err, "file %s must exist on disk after Finish", name)
+ }
+}
+
+// TestPrepareFilePart_OpensWrittenPart verifies that PrepareFilePart successfully opens a
+// previously flushed SIDX part and introduces it into the snapshot with mp == nil.
+func TestPrepareFilePart_OpensWrittenPart(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ opts := NewDefaultOptions()
+ opts.Memory = protector.NewMemory(observability.NewBypassRegistry())
+ opts.Path = tmpPath
+ opts.AvailablePartIDs = nil
+
+ sidxInstance, err := NewSIDX(fileSystem, opts)
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, sidxInstance.Close())
+ }()
+
+ // Write data and flush it to disk to get a valid on-disk part.
+ reqs := []WriteRequest{
+ createTestWriteRequest(common.SeriesID(1), 100, "payload-1",
+ Tag{Name: testTagStatus, Value: []byte("ok"), ValueType: pbv1.ValueTypeStr}),
+ createTestWriteRequest(common.SeriesID(2), 200, "payload-2",
+ Tag{Name: testTagStatus, Value: []byte("ok"), ValueType: pbv1.ValueTypeStr}),
+ }
+ mp, convErr := sidxInstance.ConvertToMemPart(reqs, 1, nil, nil)
+ require.NoError(t, convErr)
+ require.NotNil(t, mp)
+
+ partID := uint64(1)
+ destPath := partPath(tmpPath, partID)
+ mp.MustFlush(fileSystem, destPath)
+ ReleaseMemPart(mp)
+
+ // Verify core files exist before calling PrepareFilePart.
+ for _, name := range []string{primaryFilename, dataFilename, keysFilename, metaFilename} {
+ _, statErr := os.Stat(filepath.Join(destPath, name))
+ require.NoError(t, statErr, "core file %s must exist on disk", name)
+ }
+
+ // Apply PrepareFilePart by building and replacing the snapshot.
+ transitionFn := sidxInstance.PrepareFilePart(partID, destPath)
+ cur := sidxInstance.CurrentSnapshot()
+ next := transitionFn(cur)
+ if cur != nil {
+ cur.DecRef()
+ }
+ sidxInstance.ReplaceSnapshot(next)
+
+ // Verify: snapshot contains the part with nil mp (file-backed).
+ snp := sidxInstance.CurrentSnapshot()
+ require.NotNil(t, snp, "snapshot must not be nil after PrepareFilePart")
+ defer snp.DecRef()
+
+ require.Equal(t, 1, snp.getPartCount(), "snapshot must contain exactly one part")
+
+ // The part must be file-backed (mp == nil).
+ for _, pw := range snp.parts {
+ require.Nil(t, pw.mp, "part introduced via PrepareFilePart must have nil memPart")
+ require.Equal(t, partID, pw.p.partMetadata.ID, "part ID must match")
+ }
+}
diff --git a/banyand/internal/sidx/interfaces.go b/banyand/internal/sidx/interfaces.go
index e15d545de..058c8626f 100644
--- a/banyand/internal/sidx/interfaces.go
+++ b/banyand/internal/sidx/interfaces.go
@@ -37,6 +37,8 @@ import (
type SnapshotTransactionSupport interface {
// PrepareMemPart prepares a transition for introducing a memory part.
PrepareMemPart(partID uint64, mp *MemPart) func(cur *Snapshot) *Snapshot
+ // PrepareFilePart prepares a transition for introducing a file-backed part.
+ PrepareFilePart(partID uint64, partPath string) func(cur *Snapshot) *Snapshot
// PrepareFlushed prepares a transition for introducing flushed parts.
PrepareFlushed(intro *FlusherIntroduction) func(cur *Snapshot) *Snapshot
// PrepareMerged prepares a transition for introducing merged parts.
diff --git a/banyand/internal/sidx/metadata.go b/banyand/internal/sidx/metadata.go
index 58e0c252c..40b2cf521 100644
--- a/banyand/internal/sidx/metadata.go
+++ b/banyand/internal/sidx/metadata.go
@@ -28,6 +28,7 @@ import (
"github.com/apache/skywalking-banyandb/api/common"
"github.com/apache/skywalking-banyandb/banyand/internal/storage"
+ "github.com/apache/skywalking-banyandb/banyand/queue"
"github.com/apache/skywalking-banyandb/pkg/encoding"
"github.com/apache/skywalking-banyandb/pkg/fs"
"github.com/apache/skywalking-banyandb/pkg/logger"
@@ -61,6 +62,11 @@ func validatePartMetadata(fileSystem fs.FileSystem, partPath string) error {
return nil
}
+// ValidatePartMetadata reads and validates the manifest.json in the given part directory.
+func ValidatePartMetadata(fileSystem fs.FileSystem, partPath string) error {
+ return validatePartMetadata(fileSystem, partPath)
+}
+
// blockMetadata contains metadata for a block within a part.
type blockMetadata struct {
tagsBlocks map[string]dataBlock
@@ -142,6 +148,19 @@ func (pm *partMetadata) reset() {
pm.ID = 0
}
+func (pm *partMetadata) fillFromSyncContext(ctx *queue.ChunkedSyncPartContext) {
+ pm.CompressedSizeBytes = ctx.CompressedSizeBytes
+ pm.UncompressedSizeBytes = ctx.UncompressedSizeBytes
+ pm.TotalCount = ctx.TotalCount
+ pm.BlocksCount = ctx.BlocksCount
+ pm.MinKey = ctx.MinKey
+ pm.MaxKey = ctx.MaxKey
+ minTS := ctx.MinTimestamp
+ maxTS := ctx.MaxTimestamp
+ pm.MinTimestamp = &minTS
+ pm.MaxTimestamp = &maxTS
+}
+
// reset clears blockMetadata for reuse in object pool.
func (bm *blockMetadata) reset() {
bm.seriesID = 0
diff --git a/banyand/internal/sidx/part.go b/banyand/internal/sidx/part.go
index 98aa364af..f0372af6a 100644
--- a/banyand/internal/sidx/part.go
+++ b/banyand/internal/sidx/part.go
@@ -869,11 +869,13 @@ func partName(epoch uint64) string {
return fmt.Sprintf("%016x", epoch)
}
-// SyncPartContext manages multiple sidx memParts.
+// SyncPartContext manages a file-backed sidx part during streaming sync.
type SyncPartContext struct {
- memPart *memPart
- writers *writers
- name string
+ fileSystem fs.FileSystem
+ writers *writers
+ name string
+ partPath string
+ partMeta partMetadata
}
// NewSyncPartContext creates a new sidx part context.
@@ -881,11 +883,35 @@ func NewSyncPartContext() *SyncPartContext {
return &SyncPartContext{}
}
-// Set sets the memory part and writers.
-func (spc *SyncPartContext) Set(name string, memPart *memPart, writers *writers) {
+// SetForFile initializes file-backed writers for direct file writing.
+// The metadata fields are stored and written as manifest.json during Finish().
+func (spc *SyncPartContext) SetForFile(name string, fileSystem fs.FileSystem, partPath string, ctx *queue.ChunkedSyncPartContext, shouldCache bool) {
spc.name = name
- spc.memPart = memPart
- spc.writers = writers
+ spc.fileSystem = fileSystem
+ spc.partPath = partPath
+ spc.partMeta.fillFromSyncContext(ctx)
+
+ w := GenerateWriters()
+ w.MustInitForFilePart(fileSystem, partPath, shouldCache)
+ spc.writers = w
+}
+
+// Finish closes writers, writes manifest.json, and returns the on-disk part path.
+// It clears spc.partPath so a subsequent Close will not remove the files.
+func (spc *SyncPartContext) Finish() string {
+ if spc.writers != nil {
+ spc.writers.MustClose()
+ ReleaseWriters(spc.writers)
+ spc.writers = nil
+ }
+ if spc.fileSystem != nil && spc.partPath != "" {
+ spc.partMeta.mustWriteMetadata(spc.fileSystem, spc.partPath)
+ spc.fileSystem.SyncPath(spc.partPath)
+ }
+ p := spc.partPath
+ spc.partPath = ""
+ spc.fileSystem = nil
+ return p
}
// Name gets the name.
@@ -893,22 +919,22 @@ func (spc *SyncPartContext) Name() string {
return spc.name
}
-// GetMemPart gets the memory part.
-func (spc *SyncPartContext) GetMemPart() *MemPart {
- return spc.memPart
-}
-
// GetWriters gets the writers.
func (spc *SyncPartContext) GetWriters() *Writers {
return spc.writers
}
-// Close closes the sidx part context.
+// Close closes the sidx part context and cleans up any incomplete file parts on error.
func (spc *SyncPartContext) Close() {
- spc.writers.MustClose()
- ReleaseWriters(spc.writers)
- spc.writers = nil
- spc.memPart = nil
+ if spc.writers != nil {
+ spc.writers.MustClose()
+ ReleaseWriters(spc.writers)
+ spc.writers = nil
+ }
+ if spc.partPath != "" && spc.fileSystem != nil {
+ spc.fileSystem.MustRMAll(spc.partPath)
+ spc.partPath = ""
+ }
}
func removeExt(nameWithExt, ext string) string {
diff --git a/banyand/internal/sidx/sidx.go b/banyand/internal/sidx/sidx.go
index 910d85b4a..244ffbedd 100644
--- a/banyand/internal/sidx/sidx.go
+++ b/banyand/internal/sidx/sidx.go
@@ -298,6 +298,20 @@ func (s *sidx) PrepareMemPart(partID uint64, mp *MemPart) func(cur *Snapshot) *S
}
}
+// PrepareFilePart prepares a transition for introducing a file-backed part.
+func (s *sidx) PrepareFilePart(partID uint64, partPath string) func(cur *Snapshot) *Snapshot {
+ return func(cur *Snapshot) *Snapshot {
+ if cur == nil {
+ cur = &Snapshot{}
+ }
+ next := cur.copyAllTo()
+ part := mustOpenPart(partID, partPath, s.fileSystem)
+ pw := newPartWrapper(nil, part)
+ next.parts = append(next.parts, pw)
+ return next
+ }
+}
+
// PrepareFlushed prepares a transition for introducing flushed parts.
func (s *sidx) PrepareFlushed(intro *FlusherIntroduction) func(cur *Snapshot) *Snapshot {
return func(cur *Snapshot) *Snapshot {
diff --git a/banyand/measure/block_writer.go b/banyand/measure/block_writer.go
index 948e80572..a6238b3b0 100644
--- a/banyand/measure/block_writer.go
+++ b/banyand/measure/block_writer.go
@@ -113,6 +113,19 @@ func (sw *writers) MustClose() {
}
}
+func (sw *writers) mustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool) {
+ sw.reset()
+ fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
+ sw.mustCreateTagFamilyWriters = func(name string) (fs.Writer, fs.Writer) {
+ return fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagFamiliesMetadataFilenameExt), storage.FilePerm, shouldCache),
+ fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagFamiliesFilenameExt), storage.FilePerm, shouldCache)
+ }
+ sw.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
+ sw.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
+ sw.timestampsWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, timestampsFilename), storage.FilePerm, shouldCache))
+ sw.fieldValuesWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, fieldValuesFilename), storage.FilePerm, shouldCache))
+}
+
func (sw *writers) getColumnMetadataWriterAndColumnWriter(columnName string) (*writer, *writer) {
chw, ok := sw.tagFamilyMetadataWriters[columnName]
cw := sw.tagFamilyWriters[columnName]
@@ -176,18 +189,7 @@ func (bw *blockWriter) MustInitForMemPart(mp *memPart) {
func (bw *blockWriter) mustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool) {
bw.reset()
- fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
- bw.writers.mustCreateTagFamilyWriters = func(name string) (fs.Writer, fs.Writer) {
- metaPath := filepath.Join(path, name+tagFamiliesMetadataFilenameExt)
- dataPath := filepath.Join(path, name+tagFamiliesFilenameExt)
- return fs.MustCreateFile(fileSystem, metaPath, storage.FilePerm, shouldCache),
- fs.MustCreateFile(fileSystem, dataPath, storage.FilePerm, shouldCache)
- }
-
- bw.writers.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
- bw.writers.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
- bw.writers.timestampsWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, timestampsFilename), storage.FilePerm, shouldCache))
- bw.writers.fieldValuesWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, fieldValuesFilename), storage.FilePerm, shouldCache))
+ bw.writers.mustInitForFilePart(fileSystem, path, shouldCache)
}
func (bw *blockWriter) MustWriteDataPoints(sid common.SeriesID, timestamps, versions []int64, tagFamilies [][]nameValues, fields []nameValues) {
diff --git a/banyand/measure/file_part_sync_test.go b/banyand/measure/file_part_sync_test.go
new file mode 100644
index 000000000..eabe55627
--- /dev/null
+++ b/banyand/measure/file_part_sync_test.go
@@ -0,0 +1,207 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 measure
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
+ "github.com/apache/skywalking-banyandb/pkg/convert"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/test/flags"
+)
+
+func buildAndFlushMeasureMemPart(t *testing.T, fileSystem fs.FileSystem, dps *dataPoints, destPath string) {
+ t.Helper()
+ mp := generateMemPart()
+ mp.mustInitFromDataPoints(dps)
+ mp.mustFlush(fileSystem, destPath)
+ releaseMemPart(mp)
+}
+
+func makeTestDataPoints(now int64) *dataPoints {
+ return &dataPoints{
+ seriesIDs: []common.SeriesID{1, 2},
+ timestamps: []int64{now, now + 1000},
+ versions: []int64{1, 2},
+ tagFamilies: [][]nameValues{
+ {
+ {
+ name: "singleTag",
+ values: []*nameValue{
+ {name: "strTag", valueType: pbv1.ValueTypeStr, value: []byte("val1"), valueArr: nil},
+ },
+ },
+ },
+ {
+ {
+ name: "singleTag",
+ values: []*nameValue{
+ {name: "strTag", valueType: pbv1.ValueTypeStr, value: []byte("val2"), valueArr: nil},
+ },
+ },
+ },
+ },
+ fields: []nameValues{
+ {
+ name: "intField",
+ values: []*nameValue{
+ {name: "intField", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(100), valueArr: nil},
+ },
+ },
+ {
+ name: "intField",
+ values: []*nameValue{
+ {name: "intField", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(200), valueArr: nil},
+ },
+ },
+ },
+ }
+}
+
+// TestMustAddFilePart_FilesOnDisk verifies that mustAddFilePart introduces a file-backed part
+// into the snapshot with mp == nil.
+func TestMustAddFilePart_FilesOnDisk(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _ := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), testSnapshotOption(), nil)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ now := int64(1_000_000_000)
+ dps := makeTestDataPoints(now)
+
+ partID := uint64(1)
+ destPath := partPath(tabDir, partID)
+ buildAndFlushMeasureMemPart(t, fileSystem, dps, destPath)
+
+ tst.mustAddFilePart(partID)
+
+ snp := tst.currentSnapshot()
+ require.NotNil(t, snp, "snapshot must not be nil after mustAddFilePart")
+ defer snp.decRef()
+
+ require.Len(t, snp.parts, 1, "snapshot must contain exactly one part")
+ require.Equal(t, partID, snp.parts[0].p.partMetadata.ID, "part ID must match")
+ require.Nil(t, snp.parts[0].mp, "file-backed part must have nil memPart")
+}
+
+// TestIntroduceMemPart_NilMpGuard verifies that mustAddFilePart does not panic when
+// introducing a file-backed part (mp == nil) through the introducer.
+func TestIntroduceMemPart_NilMpGuard(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _ := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), testSnapshotOption(), nil)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ now := int64(2_000_000_000)
+ dps := makeTestDataPoints(now)
+
+ partID := uint64(7)
+ destPath := partPath(tabDir, partID)
+ buildAndFlushMeasureMemPart(t, fileSystem, dps, destPath)
+
+ require.NotPanics(t, func() {
+ tst.mustAddFilePart(partID)
+ })
+
+ snp := tst.currentSnapshot()
+ require.NotNil(t, snp)
+ defer snp.decRef()
+ require.Len(t, snp.parts, 1)
+ require.Nil(t, snp.parts[0].mp, "file-backed part must have nil memPart")
+}
+
+// TestFilePart_ErrorCleanup verifies that when a syncPartContext is closed without calling
+// FinishSync, the incomplete part directory is removed from disk.
+func TestFilePart_ErrorCleanup(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _ := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), testSnapshotOption(), nil)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ partID := uint64(300)
+ destPath := partPath(tabDir, partID)
+ fileSystem.MkdirPanicIfExist(destPath, storage.DirPerm)
+
+ spc := &syncPartContext{
+ tsTable: tst,
+ fileSystem: fileSystem,
+ partPath: destPath,
+ partID: partID,
+ }
+ w := generateWriters()
+ w.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, metaFilename), storage.FilePerm, false))
+ w.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, primaryFilename), storage.FilePerm, false))
+ w.timestampsWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, timestampsFilename), storage.FilePerm, false))
+ w.fieldValuesWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, fieldValuesFilename), storage.FilePerm, false))
+ spc.writers = w
+
+ spc.writers.metaWriter.MustWrite([]byte("partial"))
+
+ _, statErr := os.Stat(destPath)
+ require.NoError(t, statErr, "part directory must exist before Close")
+
+ require.NoError(t, spc.Close())
+
+ _, statErr = os.Stat(destPath)
+ require.True(t, os.IsNotExist(statErr), "incomplete part directory must be removed after Close without FinishSync")
+
+ snp := tst.currentSnapshot()
+ if snp != nil {
+ defer snp.decRef()
+ require.Empty(t, snp.parts, "no parts must be in snapshot after error cleanup")
+ }
+}
diff --git a/banyand/measure/introducer.go b/banyand/measure/introducer.go
index 8aed38c3d..92dc02c4e 100644
--- a/banyand/measure/introducer.go
+++ b/banyand/measure/introducer.go
@@ -23,12 +23,12 @@ import (
)
type introduction struct {
- memPart *partWrapper
+ part *partWrapper
applied chan struct{}
}
func (i *introduction) reset() {
- i.memPart = nil
+ i.part = nil
i.applied = nil
}
@@ -120,7 +120,7 @@ func (tst *tsTable) introducerLoop(flushCh chan *flusherIntroduction, mergeCh ch
return
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
- tst.introduceMemPart(next, epoch)
+ tst.introducePart(next, epoch)
tst.incTotalIntroduceLoopFinished("mem")
epoch++
case next := <-flushCh:
@@ -154,7 +154,7 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
return
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
- tst.introduceMemPart(next, epoch)
+ tst.introducePart(next, epoch)
tst.incTotalIntroduceLoopFinished("mem")
epoch++
case next := <-flushCh:
@@ -183,7 +183,7 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
}
}
-func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint64) {
+func (tst *tsTable) introducePart(nextIntroduction *introduction, epoch uint64) {
cur := tst.currentSnapshot()
if cur != nil {
defer cur.decRef()
@@ -191,12 +191,16 @@ func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint6
cur = new(snapshot)
}
- next := nextIntroduction.memPart
- tst.addPendingDataCount(-int64(next.mp.partMetadata.TotalCount))
+ next := nextIntroduction.part
+ if next.mp != nil {
+ tst.addPendingDataCount(-int64(next.mp.partMetadata.TotalCount))
+ }
nextSnp := cur.copyAllTo(epoch)
nextSnp.parts = append(nextSnp.parts, next)
nextSnp.creator = snapshotCreatorMemPart
- tst.replaceSnapshot(&nextSnp, false)
+ // Persist snapshot for file-backed introductions so the part survives restart.
+ // memPart introductions skip persist because the flusher persists after mem→file conversion.
+ tst.replaceSnapshot(&nextSnp, next.mp == nil)
if nextIntroduction.applied != nil {
close(nextIntroduction.applied)
}
diff --git a/banyand/measure/part_metadata.go b/banyand/measure/part_metadata.go
index dec52f160..dbd41b5f6 100644
--- a/banyand/measure/part_metadata.go
+++ b/banyand/measure/part_metadata.go
@@ -49,6 +49,15 @@ func (pm *partMetadata) reset() {
pm.ID = 0
}
+func (pm *partMetadata) fillFromSyncContext(ctx *queue.ChunkedSyncPartContext) {
+ pm.CompressedSizeBytes = ctx.CompressedSizeBytes
+ pm.UncompressedSizeBytes = ctx.UncompressedSizeBytes
+ pm.TotalCount = ctx.TotalCount
+ pm.BlocksCount = ctx.BlocksCount
+ pm.MinTimestamp = ctx.MinTimestamp
+ pm.MaxTimestamp = ctx.MaxTimestamp
+}
+
func validatePartMetadata(fileSystem fs.FileSystem, partPath string) error {
metadataPath := filepath.Join(partPath, metadataFilename)
metadata, err := fileSystem.Read(metadataPath)
diff --git a/banyand/measure/tstable.go b/banyand/measure/tstable.go
index c9054d541..6c3fff58e 100644
--- a/banyand/measure/tstable.go
+++ b/banyand/measure/tstable.go
@@ -349,14 +349,35 @@ func (tst *tsTable) mustAddDataPointsWithSegmentID(dps *dataPoints, segmentID in
tst.mustAddMemPart(mp)
}
+func (tst *tsTable) mustAddFilePart(partID uint64) {
+ p := mustOpenFilePart(partID, tst.root, tst.fileSystem)
+ p.partMetadata.ID = partID
+
+ ind := generateIntroduction()
+ defer releaseIntroduction(ind)
+ ind.applied = make(chan struct{})
+ ind.part = newPartWrapper(nil, p)
+
+ select {
+ case tst.introductions <- ind:
+ case <-tst.loopCloser.CloseNotify():
+ ind.part.decRef()
+ return
+ }
+ select {
+ case <-ind.applied:
+ case <-tst.loopCloser.CloseNotify():
+ }
+}
+
func (tst *tsTable) mustAddMemPart(mp *memPart) {
p := openMemPart(mp)
ind := generateIntroduction()
defer releaseIntroduction(ind)
ind.applied = make(chan struct{})
- ind.memPart = newPartWrapper(mp, p)
- ind.memPart.p.partMetadata.ID = atomic.AddUint64(&tst.curPartID, 1)
+ ind.part = newPartWrapper(mp, p)
+ ind.part.p.partMetadata.ID = atomic.AddUint64(&tst.curPartID, 1)
startTime := time.Now()
totalCount := mp.partMetadata.TotalCount
tst.addPendingDataCount(int64(totalCount))
@@ -364,7 +385,7 @@ func (tst *tsTable) mustAddMemPart(mp *memPart) {
case tst.introductions <- ind:
case <-tst.loopCloser.CloseNotify():
tst.addPendingDataCount(-int64(totalCount))
- ind.memPart.decRef()
+ ind.part.decRef()
return
}
select {
diff --git a/banyand/measure/write_data.go b/banyand/measure/write_data.go
index cc5b855b7..2f848f111 100644
--- a/banyand/measure/write_data.go
+++ b/banyand/measure/write_data.go
@@ -20,21 +20,26 @@ package measure
import (
"fmt"
"strings"
+ "sync/atomic"
"time"
"github.com/apache/skywalking-banyandb/api/common"
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
"github.com/apache/skywalking-banyandb/banyand/internal/storage"
"github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
"github.com/apache/skywalking-banyandb/pkg/index"
"github.com/apache/skywalking-banyandb/pkg/logger"
"github.com/apache/skywalking-banyandb/pkg/pool"
)
type syncPartContext struct {
- tsTable *tsTable
- writers *writers
- memPart *memPart
+ tsTable *tsTable
+ fileSystem fs.FileSystem
+ writers *writers
+ partPath string
+ partMeta partMetadata
+ partID uint64
}
func (s *syncPartContext) NewPartType(_ *queue.ChunkedSyncPartContext) error {
@@ -43,23 +48,36 @@ func (s *syncPartContext) NewPartType(_ *queue.ChunkedSyncPartContext) error {
}
func (s *syncPartContext) FinishSync() error {
- mp := s.memPart
- s.memPart = nil
- s.tsTable.mustAddMemPart(mp)
+ // Close writers first so file data is flushed before we write metadata.
+ s.releaseCoreWriters()
+
+ s.partMeta.mustWriteMetadata(s.fileSystem, s.partPath)
+ s.fileSystem.SyncPath(s.partPath)
+
+ s.tsTable.mustAddFilePart(s.partID)
+ s.partPath = ""
return s.Close()
}
-func (s *syncPartContext) Close() error {
+// releaseCoreWriters closes and releases the core writers, if any. Safe to call multiple times.
+func (s *syncPartContext) releaseCoreWriters() {
+ if s.writers == nil {
+ return
+ }
s.writers.MustClose()
releaseWriters(s.writers)
s.writers = nil
- if s.memPart != nil {
- // syncPartContext owns the memPart directly without partWrapper refcounting.
- // It must release via releaseMemPart, not decRef, which is used in mustAddMemPart.
- releaseMemPart(s.memPart)
- s.memPart = nil
+}
+
+func (s *syncPartContext) Close() error {
+ s.releaseCoreWriters()
+ // Clean up incomplete filePart on error (partPath is cleared on success in FinishSync)
+ if s.partPath != "" && s.fileSystem != nil {
+ s.fileSystem.MustRMAll(s.partPath)
+ s.partPath = ""
}
s.tsTable = nil
+ s.fileSystem = nil
return nil
}
@@ -103,20 +121,23 @@ func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (que
}
tsdb.Tick(ctx.MaxTimestamp)
- memPart := generateMemPart()
- memPart.partMetadata.CompressedSizeBytes = ctx.CompressedSizeBytes
- memPart.partMetadata.UncompressedSizeBytes = ctx.UncompressedSizeBytes
- memPart.partMetadata.TotalCount = ctx.TotalCount
- memPart.partMetadata.BlocksCount = ctx.BlocksCount
- memPart.partMetadata.MinTimestamp = ctx.MinTimestamp
- memPart.partMetadata.MaxTimestamp = ctx.MaxTimestamp
- writers := generateWriters()
- writers.mustInitForMemPart(memPart)
- return &syncPartContext{
- tsTable: tsTable,
- writers: writers,
- memPart: memPart,
- }, nil
+
+ partID := atomic.AddUint64(&tsTable.curPartID, 1)
+ pp := partPath(tsTable.root, partID)
+ fileSystem := tsTable.fileSystem
+
+ w := generateWriters()
+ w.mustInitForFilePart(fileSystem, pp, tsTable.pm.ShouldCache(int64(ctx.CompressedSizeBytes)))
+
+ partCtx := &syncPartContext{
+ tsTable: tsTable,
+ fileSystem: fileSystem,
+ writers: w,
+ partPath: pp,
+ partID: partID,
+ }
+ partCtx.partMeta.fillFromSyncContext(ctx)
+ return partCtx, nil
}
// HandleFileChunk implements queue.ChunkedSyncHandler for streaming file chunks.
@@ -180,15 +201,6 @@ func releaseWriters(sw *writers) {
var writersPool = pool.Register[*writers]("measure-writers")
-func (sw *writers) mustInitForMemPart(mp *memPart) {
- sw.reset()
- sw.mustCreateTagFamilyWriters = mp.mustCreateMemTagFamilyWriters
- sw.metaWriter.init(&mp.meta)
- sw.primaryWriter.init(&mp.primary)
- sw.timestampsWriter.init(&mp.timestamps)
- sw.fieldValuesWriter.init(&mp.fieldValues)
-}
-
type syncSeriesContext struct {
streamer index.ExternalSegmentStreamer
segment storage.Segment[*tsTable, *commonv1.ResourceOpts]
diff --git a/banyand/stream/block_writer.go b/banyand/stream/block_writer.go
index 48e352c83..fdc77df25 100644
--- a/banyand/stream/block_writer.go
+++ b/banyand/stream/block_writer.go
@@ -89,12 +89,17 @@ func (sw *writers) reset() {
}
}
-func (sw *writers) mustInitForMemPart(mp *memPart) {
+func (sw *writers) mustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool) {
sw.reset()
- sw.mustCreateTagFamilyWriters = mp.mustCreateMemTagFamilyWriters
- sw.metaWriter.init(&mp.meta)
- sw.primaryWriter.init(&mp.primary)
- sw.timestampsWriter.init(&mp.timestamps)
+ fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
+ sw.mustCreateTagFamilyWriters = func(name string) (fs.Writer, fs.Writer, fs.Writer) {
+ return fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagFamiliesMetadataFilenameExt), storage.FilePerm, shouldCache),
+ fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagFamiliesFilenameExt), storage.FilePerm, shouldCache),
+ fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagFamiliesFilterFilenameExt), storage.FilePerm, shouldCache)
+ }
+ sw.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
+ sw.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
+ sw.timestampsWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, timestampsFilename), storage.FilePerm, shouldCache))
}
func (sw *writers) totalBytesWritten() uint64 {
@@ -194,19 +199,7 @@ func (bw *blockWriter) MustInitForMemPart(mp *memPart) {
func (bw *blockWriter) mustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool) {
bw.reset()
- fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
- bw.writers.mustCreateTagFamilyWriters = func(name string) (fs.Writer, fs.Writer, fs.Writer) {
- metaPath := filepath.Join(path, name+tagFamiliesMetadataFilenameExt)
- dataPath := filepath.Join(path, name+tagFamiliesFilenameExt)
- fitlerPath := filepath.Join(path, name+tagFamiliesFilterFilenameExt)
- return fs.MustCreateFile(fileSystem, metaPath, storage.FilePerm, shouldCache),
- fs.MustCreateFile(fileSystem, dataPath, storage.FilePerm, shouldCache),
- fs.MustCreateFile(fileSystem, fitlerPath, storage.FilePerm, shouldCache)
- }
-
- bw.writers.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
- bw.writers.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
- bw.writers.timestampsWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, timestampsFilename), storage.FilePerm, shouldCache))
+ bw.writers.mustInitForFilePart(fileSystem, path, shouldCache)
}
func (bw *blockWriter) MustWriteElements(sid common.SeriesID, timestamps []int64, elementIDs []uint64, tagFamilies [][]tagValues) {
diff --git a/banyand/stream/file_part_sync_test.go b/banyand/stream/file_part_sync_test.go
new file mode 100644
index 000000000..bc1c47076
--- /dev/null
+++ b/banyand/stream/file_part_sync_test.go
@@ -0,0 +1,195 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 stream
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
+ "github.com/apache/skywalking-banyandb/pkg/convert"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/test/flags"
+)
+
+func buildAndFlushStreamMemPart(t *testing.T, fileSystem fs.FileSystem, es *elements, destPath string) {
+ t.Helper()
+ mp := generateMemPart()
+ mp.mustInitFromElements(es)
+ mp.mustFlush(fileSystem, destPath)
+ releaseMemPart(mp)
+}
+
+func makeTestElements(now int64) *elements {
+ return &elements{
+ seriesIDs: []common.SeriesID{1, 2},
+ timestamps: []int64{now, now + 1000},
+ elementIDs: []uint64{11, 21},
+ tagFamilies: [][]tagValues{
+ {
+ {
+ tag: "singleTag",
+ values: []*tagValue{
+ {tag: "strTag", valueType: pbv1.ValueTypeStr, value: []byte("val1"), valueArr: nil},
+ },
+ },
+ },
+ {
+ {
+ tag: "singleTag",
+ values: []*tagValue{
+ {tag: "strTag", valueType: pbv1.ValueTypeStr, value: convert.Int64ToBytes(200), valueArr: nil},
+ },
+ },
+ },
+ },
+ }
+}
+
+// TestMustAddFilePart_FilesOnDisk verifies that mustAddFilePart introduces a file-backed part
+// into the snapshot with mp == nil.
+func TestMustAddFilePart_FilesOnDisk(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _, initErr := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), streamSnapshotOption(), nil, false)
+ require.NoError(t, initErr)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ now := int64(1_000_000_000)
+ es := makeTestElements(now)
+
+ partID := uint64(1)
+ destPath := partPath(tabDir, partID)
+ buildAndFlushStreamMemPart(t, fileSystem, es, destPath)
+
+ tst.mustAddFilePart(partID)
+
+ snp := tst.currentSnapshot()
+ require.NotNil(t, snp, "snapshot must not be nil after mustAddFilePart")
+ defer snp.decRef()
+
+ require.Len(t, snp.parts, 1, "snapshot must contain exactly one part")
+ require.Equal(t, partID, snp.parts[0].p.partMetadata.ID, "part ID must match")
+ require.Nil(t, snp.parts[0].mp, "file-backed part must have nil memPart")
+}
+
+// TestIntroduceMemPart_NilMpGuard verifies that mustAddFilePart does not panic when
+// introducing a file-backed part (mp == nil) through the introducer.
+func TestIntroduceMemPart_NilMpGuard(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _, initErr := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), streamSnapshotOption(), nil, false)
+ require.NoError(t, initErr)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ now := int64(2_000_000_000)
+ es := makeTestElements(now)
+
+ partID := uint64(7)
+ destPath := partPath(tabDir, partID)
+ buildAndFlushStreamMemPart(t, fileSystem, es, destPath)
+
+ require.NotPanics(t, func() {
+ tst.mustAddFilePart(partID)
+ })
+
+ snp := tst.currentSnapshot()
+ require.NotNil(t, snp)
+ defer snp.decRef()
+ require.Len(t, snp.parts, 1)
+ require.Nil(t, snp.parts[0].mp, "file-backed part must have nil memPart")
+}
+
+// TestFilePart_ErrorCleanup verifies that when a syncPartContext is closed without calling
+// FinishSync, the incomplete part directory is removed from disk.
+func TestFilePart_ErrorCleanup(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _, initErr := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), streamSnapshotOption(), nil, false)
+ require.NoError(t, initErr)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ partID := uint64(300)
+ destPath := partPath(tabDir, partID)
+ fileSystem.MkdirPanicIfExist(destPath, storage.DirPerm)
+
+ spc := &syncPartContext{
+ tsTable: tst,
+ fileSystem: fileSystem,
+ partPath: destPath,
+ partID: partID,
+ }
+ w := generateWriters()
+ w.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, metaFilename), storage.FilePerm, false))
+ w.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, primaryFilename), storage.FilePerm, false))
+ w.timestampsWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, timestampsFilename), storage.FilePerm, false))
+ spc.writers = w
+
+ spc.writers.metaWriter.MustWrite([]byte("partial"))
+
+ _, statErr := os.Stat(destPath)
+ require.NoError(t, statErr, "part directory must exist before Close")
+
+ require.NoError(t, spc.Close())
+
+ _, statErr = os.Stat(destPath)
+ require.True(t, os.IsNotExist(statErr), "incomplete part directory must be removed after Close without FinishSync")
+
+ snp := tst.currentSnapshot()
+ if snp != nil {
+ defer snp.decRef()
+ require.Empty(t, snp.parts, "no parts must be in snapshot after error cleanup")
+ }
+}
diff --git a/banyand/stream/introducer.go b/banyand/stream/introducer.go
index a9dda7460..e2255578d 100644
--- a/banyand/stream/introducer.go
+++ b/banyand/stream/introducer.go
@@ -23,12 +23,12 @@ import (
)
type introduction struct {
- memPart *partWrapper
+ part *partWrapper
applied chan struct{}
}
func (i *introduction) reset() {
- i.memPart = nil
+ i.part = nil
i.applied = nil
}
@@ -149,7 +149,7 @@ func (tst *tsTable) introducerLoop(flushCh chan *flusherIntroduction, mergeCh ch
return
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
- tst.introduceMemPart(next, epoch)
+ tst.introducePart(next, epoch)
tst.incTotalIntroduceLoopFinished("mem")
epoch++
case next := <-flushCh:
@@ -183,7 +183,7 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
return
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
- tst.introduceMemPart(next, epoch)
+ tst.introducePart(next, epoch)
tst.incTotalIntroduceLoopFinished("mem")
epoch++
case next := <-flushCh:
@@ -212,7 +212,7 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
}
}
-func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint64) {
+func (tst *tsTable) introducePart(nextIntroduction *introduction, epoch uint64) {
cur := tst.currentSnapshot()
if cur != nil {
defer cur.decRef()
@@ -220,12 +220,19 @@ func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint6
cur = new(snapshot)
}
- next := nextIntroduction.memPart
- tst.addPendingDataCount(-int64(next.mp.partMetadata.TotalCount))
+ next := nextIntroduction.part
+ if next.mp != nil {
+ tst.addPendingDataCount(-int64(next.mp.partMetadata.TotalCount))
+ }
nextSnp := cur.copyAllTo(epoch)
nextSnp.parts = append(nextSnp.parts, next)
nextSnp.creator = snapshotCreatorMemPart
tst.replaceSnapshot(&nextSnp)
+ // Persist snapshot for file-backed introductions so the part survives restart.
+ // memPart introductions skip persist because the flusher persists after mem→file conversion.
+ if next.mp == nil {
+ tst.persistSnapshot(&nextSnp)
+ }
if nextIntroduction.applied != nil {
close(nextIntroduction.applied)
}
diff --git a/banyand/stream/part_metadata.go b/banyand/stream/part_metadata.go
index 983861d5f..25c0eaa2f 100644
--- a/banyand/stream/part_metadata.go
+++ b/banyand/stream/part_metadata.go
@@ -49,6 +49,15 @@ func (pm *partMetadata) reset() {
pm.ID = 0
}
+func (pm *partMetadata) fillFromSyncContext(ctx *queue.ChunkedSyncPartContext) {
+ pm.CompressedSizeBytes = ctx.CompressedSizeBytes
+ pm.UncompressedSizeBytes = ctx.UncompressedSizeBytes
+ pm.TotalCount = ctx.TotalCount
+ pm.BlocksCount = ctx.BlocksCount
+ pm.MinTimestamp = ctx.MinTimestamp
+ pm.MaxTimestamp = ctx.MaxTimestamp
+}
+
func validatePartMetadata(fileSystem fs.FileSystem, partPath string) error {
metadataPath := filepath.Join(partPath, metadataFilename)
metadata, err := fileSystem.Read(metadataPath)
diff --git a/banyand/stream/tstable.go b/banyand/stream/tstable.go
index 60ef8dcee..9f3f5e7a0 100644
--- a/banyand/stream/tstable.go
+++ b/banyand/stream/tstable.go
@@ -360,14 +360,35 @@ func (tst *tsTable) Close() error {
return nil
}
+func (tst *tsTable) mustAddFilePart(partID uint64) {
+ p := mustOpenFilePart(partID, tst.root, tst.fileSystem)
+ p.partMetadata.ID = partID
+
+ ind := generateIntroduction()
+ defer releaseIntroduction(ind)
+ ind.applied = make(chan struct{})
+ ind.part = newPartWrapper(nil, p)
+
+ select {
+ case tst.introductions <- ind:
+ case <-tst.loopCloser.CloseNotify():
+ ind.part.decRef()
+ return
+ }
+ select {
+ case <-ind.applied:
+ case <-tst.loopCloser.CloseNotify():
+ }
+}
+
func (tst *tsTable) mustAddMemPart(mp *memPart) {
p := openMemPart(mp)
ind := generateIntroduction()
defer releaseIntroduction(ind)
ind.applied = make(chan struct{})
- ind.memPart = newPartWrapper(mp, p)
- ind.memPart.p.partMetadata.ID = atomic.AddUint64(&tst.curPartID, 1)
+ ind.part = newPartWrapper(mp, p)
+ ind.part.p.partMetadata.ID = atomic.AddUint64(&tst.curPartID, 1)
startTime := time.Now()
totalCount := mp.partMetadata.TotalCount
tst.addPendingDataCount(int64(totalCount))
@@ -375,7 +396,7 @@ func (tst *tsTable) mustAddMemPart(mp *memPart) {
case tst.introductions <- ind:
case <-tst.loopCloser.CloseNotify():
tst.addPendingDataCount(-int64(totalCount))
- ind.memPart.decRef()
+ ind.part.decRef()
return
}
select {
diff --git a/banyand/stream/write_data.go b/banyand/stream/write_data.go
index 1eeaadded..77a0b07c5 100644
--- a/banyand/stream/write_data.go
+++ b/banyand/stream/write_data.go
@@ -20,20 +20,25 @@ package stream
import (
"fmt"
"strings"
+ "sync/atomic"
"time"
"github.com/apache/skywalking-banyandb/api/common"
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
"github.com/apache/skywalking-banyandb/banyand/internal/storage"
"github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
"github.com/apache/skywalking-banyandb/pkg/index"
"github.com/apache/skywalking-banyandb/pkg/logger"
)
type syncPartContext struct {
- tsTable *tsTable
- writers *writers
- memPart *memPart
+ tsTable *tsTable
+ fileSystem fs.FileSystem
+ writers *writers
+ partPath string
+ partMeta partMetadata
+ partID uint64
}
func (s *syncPartContext) NewPartType(_ *queue.ChunkedSyncPartContext) error {
@@ -42,23 +47,36 @@ func (s *syncPartContext) NewPartType(_ *queue.ChunkedSyncPartContext) error {
}
func (s *syncPartContext) FinishSync() error {
- mp := s.memPart
- s.memPart = nil
- s.tsTable.mustAddMemPart(mp)
+ // Close writers first so file data is flushed before we write metadata.
+ s.releaseCoreWriters()
+
+ s.partMeta.mustWriteMetadata(s.fileSystem, s.partPath)
+ s.fileSystem.SyncPath(s.partPath)
+
+ s.tsTable.mustAddFilePart(s.partID)
+ s.partPath = ""
return s.Close()
}
-func (s *syncPartContext) Close() error {
+// releaseCoreWriters closes and releases the core writers, if any. Safe to call multiple times.
+func (s *syncPartContext) releaseCoreWriters() {
+ if s.writers == nil {
+ return
+ }
s.writers.MustClose()
releaseWriters(s.writers)
s.writers = nil
- if s.memPart != nil {
- // syncPartContext owns the memPart directly without partWrapper refcounting.
- // It must release via releaseMemPart, not decRef, which is used in mustAddMemPart.
- releaseMemPart(s.memPart)
- s.memPart = nil
+}
+
+func (s *syncPartContext) Close() error {
+ s.releaseCoreWriters()
+ // Clean up incomplete filePart on error (partPath is cleared on success in FinishSync)
+ if s.partPath != "" && s.fileSystem != nil {
+ s.fileSystem.MustRMAll(s.partPath)
+ s.partPath = ""
}
s.tsTable = nil
+ s.fileSystem = nil
return nil
}
@@ -102,20 +120,23 @@ func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (que
}
tsdb.Tick(ctx.MaxTimestamp)
- memPart := generateMemPart()
- memPart.partMetadata.CompressedSizeBytes = ctx.CompressedSizeBytes
- memPart.partMetadata.UncompressedSizeBytes = ctx.UncompressedSizeBytes
- memPart.partMetadata.TotalCount = ctx.TotalCount
- memPart.partMetadata.BlocksCount = ctx.BlocksCount
- memPart.partMetadata.MinTimestamp = ctx.MinTimestamp
- memPart.partMetadata.MaxTimestamp = ctx.MaxTimestamp
- writers := generateWriters()
- writers.mustInitForMemPart(memPart)
- return &syncPartContext{
- tsTable: tsTable,
- writers: writers,
- memPart: memPart,
- }, nil
+
+ partID := atomic.AddUint64(&tsTable.curPartID, 1)
+ pp := partPath(tsTable.root, partID)
+ fileSystem := tsTable.fileSystem
+
+ w := generateWriters()
+ w.mustInitForFilePart(fileSystem, pp, tsTable.pm.ShouldCache(int64(ctx.CompressedSizeBytes)))
+
+ partCtx := &syncPartContext{
+ tsTable: tsTable,
+ fileSystem: fileSystem,
+ writers: w,
+ partPath: pp,
+ partID: partID,
+ }
+ partCtx.partMeta.fillFromSyncContext(ctx)
+ return partCtx, nil
}
// HandleFileChunk implements queue.ChunkedSyncHandler for streaming file chunks.
diff --git a/banyand/trace/block_writer.go b/banyand/trace/block_writer.go
index 46aef8c49..c4267dd58 100644
--- a/banyand/trace/block_writer.go
+++ b/banyand/trace/block_writer.go
@@ -85,12 +85,16 @@ func (sw *writers) reset() {
}
}
-func (sw *writers) mustInitForMemPart(mp *memPart) {
+func (sw *writers) mustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool) {
sw.reset()
- sw.mustCreateTagWriters = mp.mustCreateMemTagWriters
- sw.metaWriter.init(&mp.meta)
- sw.primaryWriter.init(&mp.primary)
- sw.spanWriter.init(&mp.spans)
+ fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
+ sw.mustCreateTagWriters = func(name string) (fs.Writer, fs.Writer) {
+ return fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagsMetadataFilenameExt), storage.FilePerm, shouldCache),
+ fs.MustCreateFile(fileSystem, filepath.Join(path, name+tagsFilenameExt), storage.FilePerm, shouldCache)
+ }
+ sw.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
+ sw.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
+ sw.spanWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, spansFilename), storage.FilePerm, shouldCache))
}
func (sw *writers) totalBytesWritten() uint64 {
@@ -198,17 +202,7 @@ func (bw *blockWriter) MustInitForMemPart(mp *memPart, traceSize int) {
func (bw *blockWriter) mustInitForFilePart(fileSystem fs.FileSystem, path string, shouldCache bool, traceSize int) {
bw.reset()
- fileSystem.MkdirPanicIfExist(path, storage.DirPerm)
- bw.writers.mustCreateTagWriters = func(name string) (fs.Writer, fs.Writer) {
- metaPath := filepath.Join(path, name+tagsMetadataFilenameExt)
- dataPath := filepath.Join(path, name+tagsFilenameExt)
- return fs.MustCreateFile(fileSystem, metaPath, storage.FilePerm, shouldCache),
- fs.MustCreateFile(fileSystem, dataPath, storage.FilePerm, shouldCache)
- }
-
- bw.writers.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, metaFilename), storage.FilePerm, shouldCache))
- bw.writers.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, primaryFilename), storage.FilePerm, shouldCache))
- bw.writers.spanWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(path, spansFilename), storage.FilePerm, shouldCache))
+ bw.writers.mustInitForFilePart(fileSystem, path, shouldCache)
if traceSize > 0 {
bw.traceIDFilter = &traceIDFilter{
filter: generateTraceIDBloomFilter(),
diff --git a/banyand/trace/file_part_sync_test.go b/banyand/trace/file_part_sync_test.go
new file mode 100644
index 000000000..3d010b4b0
--- /dev/null
+++ b/banyand/trace/file_part_sync_test.go
@@ -0,0 +1,549 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 trace
+
+import (
+ "os"
+ "path/filepath"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/onsi/gomega"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/test/flags"
+ "github.com/apache/skywalking-banyandb/pkg/timestamp"
+)
+
+func TestMustAddFilePart_FilesOnDisk(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _ := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), traceSnapshotOption(), nil)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ // Build a memPart with sample traces, flush it to disk to get valid binary files.
+ now := int64(1_000_000_000)
+ ts := &traces{
+ traceIDs: []string{"tid-1", "tid-2"},
+ timestamps: []int64{now, now + 1000},
+ spanIDs: []string{"s1", "s2"},
+ spans: [][]byte{[]byte("span-a"), []byte("span-b")},
+ tags: [][]*tagValue{{}, {}},
+ }
+ mp := generateMemPart()
+ mp.mustInitFromTraces(ts)
+
+ // Flush the memPart to a temporary directory to get valid binary files.
+ srcDir := filepath.Join(tmpPath, "src")
+ mp.mustFlush(fileSystem, srcDir)
+ releaseMemPart(mp)
+
+ // Verify that the source files exist on disk.
+ for _, name := range []string{metaFilename, primaryFilename, spansFilename} {
+ _, err := os.Stat(filepath.Join(srcDir, name))
+ require.NoError(t, err, "source file %s should exist", name)
+ }
+
+ // Allocate a part ID and copy files into the tsTable root the same way syncPartContext does.
+ partID := uint64(1)
+ destPath := partPath(tabDir, partID)
+ fileSystem.MkdirPanicIfExist(destPath, 0o755)
+
+ // Copy required files manually (simulating what syncPartContext does via writers).
+ copyFile := func(name string) {
+ data, readErr := fileSystem.Read(filepath.Join(srcDir, name))
+ require.NoError(t, readErr)
+ fs.MustFlush(fileSystem, data, filepath.Join(destPath, name), 0o600)
+ }
+ copyFile(metaFilename)
+ copyFile(primaryFilename)
+ copyFile(spansFilename)
+
+ // Write metadata.json.
+ pm := partMetadata{
+ CompressedSizeBytes: 10,
+ UncompressedSpanSizeBytes: 12,
+ TotalCount: 2,
+ BlocksCount: 1,
+ MinTimestamp: now,
+ MaxTimestamp: now + 1000,
+ }
+ pm.mustWriteMetadata(fileSystem, destPath)
+ fileSystem.SyncPath(destPath)
+
+ // Introduce the file part via mustAddFilePart.
+ tst.mustAddFilePart(partID, nil)
+
+ // Verify the part is in the snapshot.
+ snp := tst.currentSnapshot()
+ require.NotNil(t, snp, "snapshot must not be nil after mustAddFilePart")
+ defer snp.decRef()
+
+ require.Len(t, snp.parts, 1, "snapshot must contain exactly one part")
+ require.Equal(t, partID, snp.parts[0].p.partMetadata.ID, "part ID must match")
+ // File-backed partWrapper has nil mp field.
+ require.Nil(t, snp.parts[0].mp, "file-backed part must have nil memPart")
+}
+
+func TestMustAddFilePart_SurvivesReopenViaMustOpenFilePart(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ // Build and flush a memPart.
+ now := int64(2_000_000_000)
+ ts := &traces{
+ traceIDs: []string{"trace-x"},
+ timestamps: []int64{now},
+ spanIDs: []string{"sx"},
+ spans: [][]byte{[]byte("payload")},
+ tags: [][]*tagValue{{}},
+ }
+ mp := generateMemPart()
+ mp.mustInitFromTraces(ts)
+
+ partID := uint64(42)
+ destPath := partPath(tabDir, partID)
+ mp.mustFlush(fileSystem, destPath)
+ totalCount := mp.partMetadata.TotalCount
+ releaseMemPart(mp)
+
+ // Re-open via mustOpenFilePart.
+ p := mustOpenFilePart(partID, tabDir, fileSystem)
+ defer p.close()
+
+ require.Equal(t, partID, p.partMetadata.ID)
+ require.Equal(t, totalCount, p.partMetadata.TotalCount)
+ require.NotEmpty(t, p.primaryBlockMetadata, "primary block metadata must be loaded")
+}
+
+func TestIntroduceMemPart_NilMpGuard(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _ := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), traceSnapshotOption(), nil)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ now := int64(3_000_000_000)
+ ts := &traces{
+ traceIDs: []string{"trace-nil-guard"},
+ timestamps: []int64{now},
+ spanIDs: []string{"s0"},
+ spans: [][]byte{[]byte("data")},
+ tags: [][]*tagValue{{}},
+ }
+ mp := generateMemPart()
+ mp.mustInitFromTraces(ts)
+ partID := uint64(7)
+ destPath := partPath(tabDir, partID)
+ mp.mustFlush(fileSystem, destPath)
+ releaseMemPart(mp)
+
+ // mustAddFilePart calls introducePart with a nil-mp partWrapper.
+ // It must not panic at addPendingDataCount.
+ require.NotPanics(t, func() {
+ tst.mustAddFilePart(partID, nil)
+ })
+
+ snp := tst.currentSnapshot()
+ require.NotNil(t, snp)
+ defer snp.decRef()
+ require.Len(t, snp.parts, 1)
+ require.Nil(t, snp.parts[0].mp)
+}
+
+// buildAndFlushMemPart creates a memPart from the given traces, flushes it to destPath,
+// and returns the original TotalCount. The memPart is released before returning.
+func buildAndFlushMemPart(t *testing.T, fileSystem fs.FileSystem, ts *traces, destPath string) uint64 {
+ t.Helper()
+ mp := generateMemPart()
+ mp.mustInitFromTraces(ts)
+ totalCount := mp.partMetadata.TotalCount
+ mp.mustFlush(fileSystem, destPath)
+ releaseMemPart(mp)
+ return totalCount
+}
+
+// TestFilePart_DirectWrite_ProducesCorrectFiles verifies that streaming file chunks through
+// syncPartContext.handleTraceFileChunk and calling FinishSync produces a valid file-backed
+// part (pw.mp == nil) that survives a close/reopen cycle.
+func TestFilePart_DirectWrite_ProducesCorrectFiles(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, err := newTSTable(
+ fileSystem, tabDir, common.Position{}, logger.GetLogger("test"),
+ timestamp.TimeRange{}, traceSnapshotOption(), nil,
+ )
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ // Build a valid on-disk part in a temporary source directory (simulates the sender).
+ srcDir := filepath.Join(tmpPath, "src")
+ now := int64(5_000_000_000)
+ ts := &traces{
+ traceIDs: []string{"tid-direct-1", "tid-direct-2"},
+ timestamps: []int64{now, now + 500},
+ spanIDs: []string{"sd1", "sd2"},
+ spans: [][]byte{[]byte("span-x"), []byte("span-y")},
+ tags: [][]*tagValue{{}, {}},
+ }
+ totalCount := buildAndFlushMemPart(t, fileSystem, ts, srcDir)
+ require.Greater(t, totalCount, uint64(0))
+
+ // Open the on-disk files as a reader (simulates sender reading them for transfer).
+ fileInfos, cleanup := CreatePartFileReaderFromPath(srcDir, fileSystem)
+ defer cleanup()
+ require.NotEmpty(t, fileInfos)
+
+ // Allocate a partID and destination path (simulates what syncPartContext does).
+ partID := uint64(100)
+ destPath := partPath(tabDir, partID)
+ fileSystem.MkdirPanicIfExist(destPath, 0o755)
+
+ // Create file-backed writers the same way syncPartContext.NewPartType does.
+ w := generateWriters()
+ w.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, metaFilename), storage.FilePerm, false))
+ w.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, primaryFilename), storage.FilePerm, false))
+ w.spanWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, spansFilename), storage.FilePerm, false))
+
+ // Stream each file's content through the writers (simulates handleTraceFileChunk).
+ const chunkSize = 512
+ for i := range fileInfos {
+ fi := &fileInfos[i]
+ var wr *writer
+ switch fi.Name {
+ case traceMetaName:
+ wr = &w.metaWriter
+ case tracePrimaryName:
+ wr = &w.primaryWriter
+ case traceSpansName:
+ wr = &w.spanWriter
+ default:
+ continue
+ }
+ buf := make([]byte, chunkSize)
+ for {
+ n, readErr := fi.Reader.Read(buf)
+ if n > 0 {
+ wr.MustWrite(buf[:n])
+ }
+ if readErr != nil {
+ break
+ }
+ }
+ }
+ w.MustClose()
+ releaseWriters(w)
+
+ // Write metadata (mirrors what syncPartContext.FinishSync does).
+ pm := partMetadata{
+ TotalCount: totalCount,
+ BlocksCount: 1,
+ MinTimestamp: now,
+ MaxTimestamp: now + 500,
+ }
+ pm.mustWriteMetadata(fileSystem, destPath)
+ fileSystem.SyncPath(destPath)
+
+ // Introduce the file-backed part.
+ tst.mustAddFilePart(partID, nil)
+
+ // Verify: snapshot contains the part with nil mp (file-backed).
+ snp := tst.currentSnapshot()
+ require.NotNil(t, snp, "snapshot must exist after mustAddFilePart")
+ defer snp.decRef()
+ require.Len(t, snp.parts, 1)
+ require.Equal(t, partID, snp.parts[0].p.partMetadata.ID)
+ require.Nil(t, snp.parts[0].mp, "file-backed part must have nil memPart")
+
+ // Verify: core files exist on disk.
+ for _, name := range []string{metaFilename, primaryFilename} {
+ _, statErr := os.Stat(filepath.Join(destPath, name))
+ require.NoError(t, statErr, "file %s must exist on disk", name)
+ }
+
+ // Verify: the on-disk part files are readable via mustOpenFilePart (simulates reopen).
+ // Note: snapshot persistence to disk requires a flush cycle; here we verify
+ // that the binary files written by the sync path are well-formed and loadable.
+ p := mustOpenFilePart(partID, tabDir, fileSystem)
+ defer p.close()
+ require.Equal(t, partID, p.partMetadata.ID, "reopened part ID must match")
+ require.Equal(t, totalCount, p.partMetadata.TotalCount, "reopened part TotalCount must match")
+ require.NotEmpty(t, p.primaryBlockMetadata, "primary block metadata must be loadable from disk")
+}
+
+// TestFilePart_ConcurrentSenders verifies that two concurrent goroutines each adding a
+// filePart via mustAddFilePart produce a snapshot containing both parts with no races.
+func TestFilePart_ConcurrentSenders(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _ := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), traceSnapshotOption(), nil)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ // Prepare two parts on disk before starting the goroutines.
+ type partSpec struct {
+ path string
+ id uint64
+ }
+ specs := []partSpec{
+ {id: 201},
+ {id: 202},
+ }
+ traceSets := []*traces{
+ {
+ traceIDs: []string{"concurrent-tid-1"},
+ timestamps: []int64{6_000_000_000},
+ spanIDs: []string{"cs1"},
+ spans: [][]byte{[]byte("span-c1")},
+ tags: [][]*tagValue{{}},
+ },
+ {
+ traceIDs: []string{"concurrent-tid-2"},
+ timestamps: []int64{7_000_000_000},
+ spanIDs: []string{"cs2"},
+ spans: [][]byte{[]byte("span-c2")},
+ tags: [][]*tagValue{{}},
+ },
+ }
+ for i := range specs {
+ specs[i].path = partPath(tabDir, specs[i].id)
+ buildAndFlushMemPart(t, fileSystem, traceSets[i], specs[i].path)
+ }
+
+ // Both goroutines call mustAddFilePart concurrently.
+ var wg sync.WaitGroup
+ for i := range specs {
+ wg.Add(1)
+ spec := specs[i]
+ go func() {
+ defer wg.Done()
+ tst.mustAddFilePart(spec.id, nil)
+ }()
+ }
+ wg.Wait()
+
+ // All parts must appear in the snapshot.
+ var snp *snapshot
+ require.Eventually(t, func() bool {
+ snp = tst.currentSnapshot()
+ if snp == nil {
+ return false
+ }
+ if len(snp.parts) == 2 {
+ return true
+ }
+ snp.decRef()
+ snp = nil
+ return false
+ }, flags.EventuallyTimeout, 10*time.Millisecond, "both parts must be in snapshot")
+ defer snp.decRef()
+
+ ids := make(map[uint64]struct{})
+ for _, pw := range snp.parts {
+ ids[pw.p.partMetadata.ID] = struct{}{}
+ assert.Nil(t, pw.mp, "part %d must be file-backed", pw.p.partMetadata.ID)
+ }
+ assert.Contains(t, ids, specs[0].id, "first sender's part must be present")
+ assert.Contains(t, ids, specs[1].id, "second sender's part must be present")
+}
+
+// TestFilePart_ErrorCleanup verifies that when a syncPartContext is closed without calling
+// FinishSync (e.g. gRPC stream cancellation), the incomplete part directory is removed from disk.
+func TestFilePart_ErrorCleanup(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, _ := initTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), traceSnapshotOption(), nil)
+ tst.startLoop(1)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ // Simulate what CreatePartHandler does: allocate a partID and create file-backed writers.
+ partID := uint64(300)
+ destPath := partPath(tabDir, partID)
+ fileSystem.MkdirPanicIfExist(destPath, storage.DirPerm)
+
+ spc := &syncPartContext{
+ tsTable: tst,
+ fileSystem: fileSystem,
+ partPath: destPath,
+ partID: partID,
+ }
+ w := generateWriters()
+ w.metaWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, metaFilename), storage.FilePerm, false))
+ w.primaryWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, primaryFilename), storage.FilePerm, false))
+ w.spanWriter.init(fs.MustCreateFile(fileSystem, filepath.Join(destPath, spansFilename), storage.FilePerm, false))
+ spc.writers = w
+
+ // Write partial data (simulates a partial transfer before cancellation).
+ spc.writers.metaWriter.MustWrite([]byte("partial"))
+
+ // The directory must exist before Close.
+ _, statErr := os.Stat(destPath)
+ require.NoError(t, statErr, "part directory must exist before Close")
+
+ // Close without FinishSync (simulates gRPC stream cancellation).
+ require.NoError(t, spc.Close())
+
+ // The incomplete part directory must be cleaned up.
+ _, statErr = os.Stat(destPath)
+ require.True(t, os.IsNotExist(statErr), "incomplete part directory must be removed after Close without FinishSync")
+
+ // No parts must appear in the snapshot.
+ snp := tst.currentSnapshot()
+ if snp != nil {
+ defer snp.decRef()
+ assert.Empty(t, snp.parts, "no parts must be in snapshot after error cleanup")
+ }
+}
+
+// TestFilePart_FlusherSkipsFileParts verifies that the flusher does not attempt to re-flush
+// a file-backed part (pw.mp == nil), leaving it unchanged in the snapshot.
+func TestFilePart_FlusherSkipsFileParts(t *testing.T) {
+ require.NoError(t, logger.Init(logger.Logging{Env: "dev", Level: flags.LogLevel}))
+
+ fileSystem := fs.NewLocalFileSystem()
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ // Use a non-zero flushTimeout so the flusher loop actually runs.
+ opt := option{
+ flushTimeout: 10 * time.Millisecond,
+ mergePolicy: newDefaultMergePolicyForTesting(),
+ protector: traceSnapshotOption().protector,
+ }
+ tst, err := newTSTable(fileSystem, tabDir, common.Position{}, logger.GetLogger("test"), timestamp.TimeRange{}, opt, nil)
+ require.NoError(t, err)
+ defer func() {
+ require.NoError(t, tst.Close())
+ }()
+
+ // Build and flush a memPart to disk, then introduce it as a filePart.
+ partID := uint64(400)
+ destPath := partPath(tabDir, partID)
+ now := int64(8_000_000_000)
+ ts := &traces{
+ traceIDs: []string{"flusher-skip-tid"},
+ timestamps: []int64{now},
+ spanIDs: []string{"fs1"},
+ spans: [][]byte{[]byte("span-fs")},
+ tags: [][]*tagValue{{}},
+ }
+ buildAndFlushMemPart(t, fileSystem, ts, destPath)
+ tst.mustAddFilePart(partID, nil)
+
+ // Confirm the part is in the snapshot with nil mp before the flusher runs.
+ snpBefore := tst.currentSnapshot()
+ require.NotNil(t, snpBefore)
+ require.Len(t, snpBefore.parts, 1)
+ require.Nil(t, snpBefore.parts[0].mp, "part must be file-backed before flusher cycle")
+ snpBefore.decRef()
+
+ // Assert that across many flusher cycles (flushTimeout=10ms over 500ms window), the
+ // snapshot continuously shows exactly one file-backed part — i.e., the flusher never
+ // mutates it into a memPart nor creates a duplicate.
+ g := gomega.NewWithT(t)
+ g.Consistently(func() bool {
+ snp := tst.currentSnapshot()
+ if snp == nil {
+ return false
+ }
+ defer snp.decRef()
+ if len(snp.parts) != 1 {
+ return false
+ }
+ if snp.parts[0].mp != nil {
+ return false
+ }
+ return snp.parts[0].p.partMetadata.ID == partID
+ }).WithTimeout(500 * time.Millisecond).WithPolling(20 * time.Millisecond).Should(gomega.BeTrue())
+
+ snpAfter := tst.currentSnapshot()
+ require.NotNil(t, snpAfter)
+ defer snpAfter.decRef()
+
+ // Exactly one on-disk part directory must exist (no duplicates).
+ entries := fileSystem.ReadDir(tabDir)
+ partDirs := 0
+ for _, e := range entries {
+ if e.IsDir() && e.Name() != sidxDirName && e.Name() != storage.FailedPartsDirName {
+ partDirs++
+ }
+ }
+ assert.Equal(t, 1, partDirs, "flusher must not create a second part directory for a file-backed part")
+}
diff --git a/banyand/trace/introducer.go b/banyand/trace/introducer.go
index e33bfb52e..93e23da08 100644
--- a/banyand/trace/introducer.go
+++ b/banyand/trace/introducer.go
@@ -25,15 +25,17 @@ import (
)
type introduction struct {
- memPart *partWrapper
- applied chan struct{}
- sidxReqsMap map[string]*sidx.MemPart
+ part *partWrapper
+ applied chan struct{}
+ sidxReqsMap map[string]*sidx.MemPart
+ sidxFilePartsMap map[string]string
}
func (i *introduction) reset() {
- i.memPart = nil
+ i.part = nil
i.applied = nil
i.sidxReqsMap = nil
+ i.sidxFilePartsMap = nil
}
var introductionPool = pool.Register[*introduction]("trace-introduction")
@@ -160,7 +162,7 @@ func (tst *tsTable) introducerLoop(flushCh chan *flusherIntroduction, mergeCh ch
return
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
- tst.introduceMemPart(next, epoch)
+ tst.introducePart(next, epoch)
tst.incTotalIntroduceLoopFinished("mem")
epoch++
case next := <-flushCh:
@@ -194,7 +196,7 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
return
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
- tst.introduceMemPart(next, epoch)
+ tst.introducePart(next, epoch)
tst.incTotalIntroduceLoopFinished("mem")
epoch++
case next := <-flushCh:
@@ -223,16 +225,21 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
}
}
-func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint64) {
+func (tst *tsTable) introducePart(nextIntroduction *introduction, epoch uint64) {
// Create generic transaction
txn := snapshotpkg.NewTransaction()
defer txn.Release()
// Prepare trace snapshot transition
- next := nextIntroduction.memPart
- tst.addPendingDataCount(-int64(next.mp.partMetadata.TotalCount))
+ next := nextIntroduction.part
+ if next.mp != nil {
+ tst.addPendingDataCount(-int64(next.mp.partMetadata.TotalCount))
+ }
partID := next.p.partMetadata.ID
+ // Capture the snapshot built by the transaction so we can persist it directly
+ // after commit, without re-acquiring a reference via currentSnapshot().
+ var traceNextSnp *snapshot
traceTransition := snapshotpkg.NewTransition(tst, func(cur *snapshot) *snapshot {
if cur == nil {
cur = new(snapshot)
@@ -240,7 +247,8 @@ func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint6
nextSnp := cur.copyAllTo(epoch)
nextSnp.parts = append(nextSnp.parts, next)
nextSnp.creator = snapshotCreatorMemPart
- return &nextSnp
+ traceNextSnp = &nextSnp
+ return traceNextSnp
})
defer traceTransition.Release()
snapshotpkg.AddTransition(txn, traceTransition)
@@ -254,6 +262,13 @@ func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint6
sidxTransitions = append(sidxTransitions, sidxTransition)
snapshotpkg.AddTransition(txn, sidxTransition)
}
+ for name, sidxPartPath := range nextIntroduction.sidxFilePartsMap {
+ sidxInstance := tst.mustGetOrCreateSidx(name)
+ prepareFunc := sidxInstance.PrepareFilePart(partID, sidxPartPath)
+ sidxTransition := snapshotpkg.NewTransition(sidxInstance, prepareFunc)
+ sidxTransitions = append(sidxTransitions, sidxTransition)
+ snapshotpkg.AddTransition(txn, sidxTransition)
+ }
defer func() {
for _, t := range sidxTransitions {
t.Release()
@@ -263,6 +278,12 @@ func (tst *tsTable) introduceMemPart(nextIntroduction *introduction, epoch uint6
// Commit all atomically under single transaction lock
txn.Commit()
+ // Persist snapshot for file-backed introductions so the part survives restart.
+ // memPart introductions skip persist because the flusher persists after mem→file conversion.
+ if next.mp == nil && traceNextSnp != nil {
+ tst.persistSnapshot(traceNextSnp)
+ }
+
if nextIntroduction.applied != nil {
close(nextIntroduction.applied)
}
diff --git a/banyand/trace/migration_test.go b/banyand/trace/migration_test.go
new file mode 100644
index 000000000..2ea48107e
--- /dev/null
+++ b/banyand/trace/migration_test.go
@@ -0,0 +1,1106 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 trace
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "io"
+ "path/filepath"
+ "sort"
+ "testing"
+ "time"
+
+ "github.com/onsi/gomega"
+ "github.com/onsi/gomega/gleak"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ "github.com/apache/skywalking-banyandb/banyand/internal/sidx"
+ "github.com/apache/skywalking-banyandb/banyand/protector"
+ "github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/convert"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
+ "github.com/apache/skywalking-banyandb/pkg/query/model"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/timestamp"
+)
+
+// mustNewSpace creates a temp directory for tests, failing on error.
+func mustNewSpace(g *gomega.WithT) (string, func()) {
+ dir, cleanup, err := test.NewSpace()
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ return dir, cleanup
+}
+
+// transferParts performs the file-based migration of all parts from sender snapshot to receiver tsTable.
+// For each part it reads the on-disk files, streams chunks through the real syncChunkCallback handler,
+// and calls FinishSync so that the receiver gains a file-backed part.
+// sidxAllPaths is a pre-captured map[sidxName]map[partID]path that was obtained atomically with senderSnp
+// (via waitForConsistentSidxSnapshot). Pass nil to skip SIDX transfer (for tests without SIDX).
+func transferParts(g *gomega.WithT, senderSnp *snapshot, receiverTst *tsTable, fileSystem fs.FileSystem, sidxAllPaths map[string]map[uint64]string) {
+ handler := &syncChunkCallback{l: logger.GetLogger("transfer-handler")}
+
+ buf := make([]byte, 64*1024)
+ streamFiles := func(files []queue.FileInfo, ctx *queue.ChunkedSyncPartContext) {
+ for i := range files {
+ fi := &files[i]
+ ctx.FileName = fi.Name
+ for {
+ n, readErr := fi.Reader.Read(buf)
+ if n > 0 {
+ g.Expect(handler.HandleFileChunk(ctx, buf[:n])).To(gomega.Succeed())
+ }
+ if errors.Is(readErr, io.EOF) {
+ break
+ }
+ g.Expect(readErr).NotTo(gomega.HaveOccurred())
+ }
+ }
+ }
+
+ for _, pw := range senderSnp.parts {
+ if pw.mp != nil {
+ // Only transfer file-backed parts.
+ continue
+ }
+
+ partID := pw.p.partMetadata.ID
+ coreFiles, cleanup := CreatePartFileReaderFromPath(pw.p.path, fileSystem)
+
+ // Load core part metadata from disk via the same helper used by lifecycle migration.
+ coreData, parseErr := ParsePartMetadata(fileSystem, pw.p.path)
+ g.Expect(parseErr).NotTo(gomega.HaveOccurred())
+ ctx := &queue.ChunkedSyncPartContext{
+ ID: coreData.ID,
+ CompressedSizeBytes: coreData.CompressedSizeBytes,
+ UncompressedSizeBytes: coreData.UncompressedSizeBytes,
+ TotalCount: coreData.TotalCount,
+ BlocksCount: coreData.BlocksCount,
+ MinTimestamp: coreData.MinTimestamp,
+ MaxTimestamp: coreData.MaxTimestamp,
+ PartType: PartTypeCore,
+ }
+
+ partCtx := &syncPartContext{tsTable: receiverTst, l: logger.GetLogger("transfer-part")}
+
+ // Transfer SIDX parts first so they share the same partID as the core part
+ // (production order: SIDX parts arrive before core part in VisitShard).
+ for sidxName, pathsByID := range sidxAllPaths {
+ sidxPartPath, ok := pathsByID[partID]
+ if !ok {
+ continue
+ }
+ sidxData, sidxParseErr := sidx.ParsePartMetadata(fileSystem, sidxPartPath)
+ g.Expect(sidxParseErr).NotTo(gomega.HaveOccurred())
+ sidxCtx := &queue.ChunkedSyncPartContext{
+ ID: sidxData.ID,
+ CompressedSizeBytes: sidxData.CompressedSizeBytes,
+ UncompressedSizeBytes: sidxData.UncompressedSizeBytes,
+ TotalCount: sidxData.TotalCount,
+ BlocksCount: sidxData.BlocksCount,
+ MinKey: sidxData.MinKey,
+ MaxKey: sidxData.MaxKey,
+ PartType: sidxName,
+ }
+ g.Expect(partCtx.NewPartType(sidxCtx)).To(gomega.Succeed())
+ sidxCtx.Handler = partCtx
+
+ sidxFiles, sidxCleanup := sidx.CreatePartFileReaderFromPath(sidxPartPath, fileSystem)
+ streamFiles(sidxFiles, sidxCtx)
+ sidxCleanup()
+ }
+
+ // Transfer core part.
+ g.Expect(partCtx.NewPartType(ctx)).To(gomega.Succeed())
+ ctx.Handler = partCtx
+ streamFiles(coreFiles, ctx)
+ cleanup()
+
+ g.Expect(partCtx.FinishSync()).To(gomega.Succeed())
+ }
+}
+
+// readPartFiles reads every regular file in partPath into a name→bytes map.
+// Directories are skipped. Intended for byte-level comparison of two migrated parts.
+func readPartFiles(g *gomega.WithT, fileSystem fs.FileSystem, partPath string) map[string][]byte {
+ entries := fileSystem.ReadDir(partPath)
+ files := make(map[string][]byte, len(entries))
+ for _, e := range entries {
+ if e.IsDir() {
+ continue
+ }
+ data, err := fileSystem.Read(filepath.Join(partPath, e.Name()))
+ g.Expect(err).NotTo(gomega.HaveOccurred(), "read %s/%s", partPath, e.Name())
+ files[e.Name()] = data
+ }
+ return files
+}
+
+// assertFileSetsEqual verifies that sender and receiver produced byte-identical files,
+// excluding the manifest file whose ID/local-state may legitimately differ.
+func assertFileSetsEqual(g *gomega.WithT, senderFiles, receiverFiles map[string][]byte, manifestName, context string) {
+ g.Expect(receiverFiles).To(gomega.HaveLen(len(senderFiles)), "[%s] file count", context)
+ for name, senderBytes := range senderFiles {
+ receiverBytes, ok := receiverFiles[name]
+ g.Expect(ok).To(gomega.BeTrue(), "[%s] receiver missing file %q", context, name)
+ if name == manifestName {
+ continue
+ }
+ g.Expect(receiverBytes).To(gomega.Equal(senderBytes), "[%s] file %q bytes mismatch", context, name)
+ }
+ for name := range receiverFiles {
+ _, ok := senderFiles[name]
+ g.Expect(ok).To(gomega.BeTrue(), "[%s] receiver has extra file %q", context, name)
+ }
+}
+
+// assertCorePartsEqual verifies a receiver core trace part byte-matches the sender's,
+// and that every non-ID field in metadata.json round-trips faithfully.
+func assertCorePartsEqual(g *gomega.WithT, fileSystem fs.FileSystem, senderPath, receiverPath string) {
+ senderMeta, err := ParsePartMetadata(fileSystem, senderPath)
+ g.Expect(err).NotTo(gomega.HaveOccurred(), "parse sender core metadata.json at %s", senderPath)
+ receiverMeta, err := ParsePartMetadata(fileSystem, receiverPath)
+ g.Expect(err).NotTo(gomega.HaveOccurred(), "parse receiver core metadata.json at %s", receiverPath)
+
+ g.Expect(receiverMeta.CompressedSizeBytes).To(gomega.Equal(senderMeta.CompressedSizeBytes), "core CompressedSizeBytes")
+ g.Expect(receiverMeta.UncompressedSizeBytes).To(gomega.Equal(senderMeta.UncompressedSizeBytes), "core UncompressedSizeBytes")
+ g.Expect(receiverMeta.TotalCount).To(gomega.Equal(senderMeta.TotalCount), "core TotalCount")
+ g.Expect(receiverMeta.BlocksCount).To(gomega.Equal(senderMeta.BlocksCount), "core BlocksCount")
+ g.Expect(receiverMeta.MinTimestamp).To(gomega.Equal(senderMeta.MinTimestamp), "core MinTimestamp")
+ g.Expect(receiverMeta.MaxTimestamp).To(gomega.Equal(senderMeta.MaxTimestamp), "core MaxTimestamp")
+
+ senderFiles := readPartFiles(g, fileSystem, senderPath)
+ receiverFiles := readPartFiles(g, fileSystem, receiverPath)
+ assertFileSetsEqual(g, senderFiles, receiverFiles, metadataFilename, "core "+senderPath)
+}
+
+// assertSidxPartsEqual verifies a receiver SIDX part byte-matches the sender's,
+// and that every non-ID field in manifest.json round-trips faithfully — including
+// SegmentID, which is smuggled through the MinTimestamp field in the sync protocol.
+func assertSidxPartsEqual(g *gomega.WithT, fileSystem fs.FileSystem, senderPath, receiverPath string) {
+ senderMeta, err := sidx.ParsePartMetadata(fileSystem, senderPath)
+ g.Expect(err).NotTo(gomega.HaveOccurred(), "parse sender sidx manifest.json at %s", senderPath)
+ receiverMeta, err := sidx.ParsePartMetadata(fileSystem, receiverPath)
+ g.Expect(err).NotTo(gomega.HaveOccurred(), "parse receiver sidx manifest.json at %s", receiverPath)
+
+ g.Expect(receiverMeta.CompressedSizeBytes).To(gomega.Equal(senderMeta.CompressedSizeBytes), "sidx CompressedSizeBytes")
+ g.Expect(receiverMeta.UncompressedSizeBytes).To(gomega.Equal(senderMeta.UncompressedSizeBytes), "sidx UncompressedSizeBytes")
+ g.Expect(receiverMeta.TotalCount).To(gomega.Equal(senderMeta.TotalCount), "sidx TotalCount")
+ g.Expect(receiverMeta.BlocksCount).To(gomega.Equal(senderMeta.BlocksCount), "sidx BlocksCount")
+ g.Expect(receiverMeta.MinKey).To(gomega.Equal(senderMeta.MinKey), "sidx MinKey")
+ g.Expect(receiverMeta.MaxKey).To(gomega.Equal(senderMeta.MaxKey), "sidx MaxKey")
+ // The sender hijacks StreamingPartData.MinTimestamp to carry SegmentID (see sidx/sync.go),
+ // so this assertion is what proves SegmentID round-trips through the sync protocol.
+ g.Expect(receiverMeta.MinTimestamp).To(gomega.Equal(senderMeta.MinTimestamp), "sidx MinTimestamp")
+ g.Expect(receiverMeta.MaxTimestamp).To(gomega.Equal(senderMeta.MaxTimestamp), "sidx MaxTimestamp")
+
+ senderFiles := readPartFiles(g, fileSystem, senderPath)
+ receiverFiles := readPartFiles(g, fileSystem, receiverPath)
+ assertFileSetsEqual(g, senderFiles, receiverFiles, "manifest.json", "sidx "+senderPath)
+}
+
+// pairSnapshotParts returns sender→receiver part path pairs. The sender sorts file-backed
+// parts by partID ASC before streaming (syncer.executeSyncOperation), and the receiver
+// allocates local partIDs monotonically as each part arrives (CreatePartHandler), so sorting
+// both sides by partID ASC yields 1-1 pairing in transfer order — robust against any future
+// change in snapshot.parts iteration order.
+func pairSnapshotParts(g *gomega.WithT, senderSnp *snapshot, receiverTst *tsTable) []struct{ senderPath, receiverPath string } {
+ var senderParts []*partWrapper
+ for _, pw := range senderSnp.parts {
+ if pw.mp == nil {
+ senderParts = append(senderParts, pw)
+ }
+ }
+ sort.Slice(senderParts, func(i, j int) bool {
+ return senderParts[i].p.partMetadata.ID < senderParts[j].p.partMetadata.ID
+ })
+ receiverSnp := receiverTst.currentSnapshot()
+ g.Expect(receiverSnp).NotTo(gomega.BeNil(), "receiver snapshot must exist after migration")
+ defer receiverSnp.decRef()
+ g.Expect(len(receiverSnp.parts)).To(gomega.Equal(len(senderParts)), "receiver part count must match sender")
+
+ receiverParts := make([]*partWrapper, len(receiverSnp.parts))
+ copy(receiverParts, receiverSnp.parts)
+ sort.Slice(receiverParts, func(i, j int) bool {
+ return receiverParts[i].p.partMetadata.ID < receiverParts[j].p.partMetadata.ID
+ })
+
+ pairs := make([]struct{ senderPath, receiverPath string }, 0, len(senderParts))
+ for i := range senderParts {
+ pairs = append(pairs, struct{ senderPath, receiverPath string }{
+ senderPath: senderParts[i].p.path,
+ receiverPath: receiverParts[i].p.path,
+ })
+ }
+ return pairs
+}
+
+// assertMigrationPreservesAllFiles walks every file-backed part in the sender snapshot and
+// asserts byte-identical migration on the receiver side: every file, every metadata field.
+// senderSidxPaths is the sender's SIDX part paths captured atomically via
+// waitForConsistentSidxSnapshot; pass nil for tests without SIDX. The function derives the
+// matching receiver SIDX paths from the receiver's tsTable so callers don't need to plumb them.
+func assertMigrationPreservesAllFiles(g *gomega.WithT, fileSystem fs.FileSystem, senderSnp *snapshot,
+ receiverTst *tsTable, senderSidxPaths map[string]map[uint64]string,
+) {
+ pairs := pairSnapshotParts(g, senderSnp, receiverTst)
+ for _, p := range pairs {
+ assertCorePartsEqual(g, fileSystem, p.senderPath, p.receiverPath)
+ }
+
+ if len(senderSidxPaths) == 0 {
+ return
+ }
+
+ // SIDX parts: pair by sender partID→receiver partID via iteration order.
+ senderCoreIDs := make([]uint64, 0, len(pairs))
+ for _, pw := range senderSnp.parts {
+ if pw.mp == nil {
+ senderCoreIDs = append(senderCoreIDs, pw.p.partMetadata.ID)
+ }
+ }
+ receiverSnp := receiverTst.currentSnapshot()
+ g.Expect(receiverSnp).NotTo(gomega.BeNil())
+ defer receiverSnp.decRef()
+ receiverCoreIDs := make([]uint64, 0, len(pairs))
+ receiverPartIDSet := make(map[uint64]struct{}, len(pairs))
+ for _, pw := range receiverSnp.parts {
+ receiverCoreIDs = append(receiverCoreIDs, pw.p.partMetadata.ID)
+ receiverPartIDSet[pw.p.partMetadata.ID] = struct{}{}
+ }
+ receiverSidxMap := receiverTst.getAllSidx()
+
+ for name, senderPathsByID := range senderSidxPaths {
+ receiverInst, ok := receiverSidxMap[name]
+ g.Expect(ok).To(gomega.BeTrue(), "receiver missing sidx instance %q", name)
+ receiverPathsByID := receiverInst.PartPaths(receiverPartIDSet)
+ for i, senderID := range senderCoreIDs {
+ senderSidxPath, hasSender := senderPathsByID[senderID]
+ if !hasSender {
+ continue
+ }
+ receiverID := receiverCoreIDs[i]
+ receiverSidxPath, hasReceiver := receiverPathsByID[receiverID]
+ g.Expect(hasReceiver).To(gomega.BeTrue(), "receiver sidx %q missing part for receiver partID %d", name, receiverID)
+ assertSidxPartsEqual(g, fileSystem, senderSidxPath, receiverSidxPath)
+ }
+ }
+}
+
+// reopenTSTable closes the existing tsTable and opens a new one at the same root.
+// This simulates a process restart and exercises the snapshot persistence path.
+func reopenTSTable(g *gomega.WithT, tst *tsTable, fileSystem fs.FileSystem, path string, loggerName string) *tsTable {
+ g.Expect(tst.Close()).To(gomega.Succeed())
+ reopened, err := newTSTable(
+ fileSystem, path, common.Position{},
+ logger.GetLogger(loggerName), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred(), "reopen tsTable at %s", path)
+ return reopened
+}
+
+// mustBuildSidxMemPart creates a sidx.MemPart for the given index name and write requests.
+// This simulates what write_standalone.go does when processing index rules.
+func mustBuildSidxMemPart(g *gomega.WithT, tst *tsTable, sidxName string, reqs []sidx.WriteRequest, minTS, maxTS int64) *sidx.MemPart {
+ sidxInstance, err := tst.getOrCreateSidx(sidxName)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ mp, err := sidxInstance.ConvertToMemPart(reqs, 0, &minTS, &maxTS)
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ return mp
+}
+
+// snapshotPartIDSet returns a set of all file-backed part IDs in the snapshot.
+func snapshotPartIDSet(snp *snapshot) map[uint64]struct{} {
+ ids := make(map[uint64]struct{}, len(snp.parts))
+ for _, pw := range snp.parts {
+ if pw.mp == nil {
+ ids[pw.p.partMetadata.ID] = struct{}{}
+ }
+ }
+ return ids
+}
+
+// waitForFileParts blocks until the tsTable has at least one file-backed part (no memParts)
+// or the timeout elapses.
+func waitForFileParts(g *gomega.WithT, tst *tsTable) {
+ g.Eventually(func() bool {
+ snp := tst.currentSnapshot()
+ if snp == nil {
+ return false
+ }
+ defer snp.decRef()
+ if len(snp.parts) == 0 {
+ return false
+ }
+ for _, pw := range snp.parts {
+ if pw.mp != nil {
+ return false
+ }
+ }
+ return true
+ }).WithTimeout(30 * time.Second).WithPolling(100 * time.Millisecond).Should(gomega.BeTrue())
+}
+
+// waitForConsistentSidxSnapshot polls until the tsTable's core snapshot and all named SIDX instances
+// are consistent: every file-backed core part ID has a corresponding file-backed SIDX part.
+// Returns the snapshot (caller must call decRef) and a map[sidxName]map[partID]path captured at the
+// same moment the consistency check passed, so they are guaranteed to be in sync.
+func waitForConsistentSidxSnapshot(g *gomega.WithT, tst *tsTable, sidxNames []string) (*snapshot, map[string]map[uint64]string) {
+ var resultSnp *snapshot
+ var resultPaths map[string]map[uint64]string
+
+ g.Eventually(func() bool {
+ snp := tst.currentSnapshot()
+ if snp == nil {
+ return false
+ }
+ coreIDs := snapshotPartIDSet(snp)
+ if len(coreIDs) == 0 {
+ snp.decRef()
+ return false
+ }
+ sidxMap := tst.getAllSidx()
+ captured := make(map[string]map[uint64]string, len(sidxNames))
+ for _, name := range sidxNames {
+ inst, ok := sidxMap[name]
+ if !ok {
+ snp.decRef()
+ return false
+ }
+ paths := inst.PartPaths(coreIDs)
+ if len(paths) == 0 {
+ snp.decRef()
+ return false
+ }
+ captured[name] = paths
+ }
+ // All SIDX instances are consistent with the core snapshot. Keep the ref.
+ if resultSnp != nil {
+ resultSnp.decRef()
+ }
+ resultSnp = snp
+ resultPaths = captured
+ return true
+ }).WithTimeout(30 * time.Second).WithPolling(50 * time.Millisecond).Should(gomega.BeTrue())
+
+ return resultSnp, resultPaths
+}
+
+// queryTraceID executes the standard query pattern used in query_test.go and returns all TraceResults.
+// The timestamp range is fixed at [1, 2] since every migration test writes traces at timestamp 1 or 2.
+func queryTraceID(g *gomega.WithT, snp *snapshot, traceID string) []model.TraceResult {
+ pp, _ := snp.getParts(nil, 1, 2, []string{traceID})
+
+ bma := generateBlockMetadataArray()
+ defer releaseBlockMetadataArray(bma)
+
+ ti := &tstIter{}
+ groupedTids := make([][]string, len(pp))
+ for i := range groupedTids {
+ groupedTids[i] = []string{traceID}
+ }
+ ti.init(bma, pp, groupedTids)
+
+ queryOpts := queryOptions{
+ schemaTagTypes: testSchemaTagTypes,
+ }
+
+ var cursors []*blockCursor
+ for ti.nextBlock() {
+ bc := generateBlockCursor()
+ p := ti.piPool[ti.idx]
+ opts := queryOpts
+ opts.TagProjection = allTagProjections
+ bc.init(p.p, p.curBlock, opts)
+ cursors = append(cursors, bc)
+ }
+ g.Expect(ti.Error()).NotTo(gomega.HaveOccurred())
+
+ cursorCh := make(chan scanCursorResult, len(cursors))
+ for _, bc := range cursors {
+ cursorCh <- scanCursorResult{cursor: bc}
+ }
+ close(cursorCh)
+
+ cursorBatch := make(chan *scanBatch, 1)
+ traceIDMap := make(map[uint64][]string)
+ traceIDMap[0] = []string{traceID}
+ cursorBatch <- &scanBatch{
+ traceBatch: traceBatch{
+ traceIDs: traceIDMap,
+ traceIDsOrder: []string{traceID},
+ keys: map[string]int64{traceID: 0},
+ },
+ cursorCh: cursorCh,
+ }
+ close(cursorBatch)
+
+ result := queryResult{
+ ctx: context.Background(),
+ tagProjection: allTagProjections,
+ cursorBatchCh: cursorBatch,
+ keys: map[string]int64{traceID: 0},
+ }
+ defer result.Release()
+
+ var got []model.TraceResult
+ for {
+ r := result.Pull()
+ if r == nil {
+ break
+ }
+ got = append(got, *r)
+ }
+ return got
+}
+
+// checkGoroutineLeak captures the set of live goroutines and registers a t.Cleanup
+// that fails the test if any new goroutine remains after the test returns.
+func checkGoroutineLeak(t *testing.T, g *gomega.WithT) {
+ t.Helper()
+ goods := gleak.Goroutines()
+ t.Cleanup(func() {
+ g.Eventually(gleak.Goroutines, 10*time.Second).ShouldNot(gleak.HaveLeaked(goods))
+ })
+}
+
+// TestLifecycleMigration_PreserveTraceData verifies that file-based migration preserves trace data correctly.
+func TestLifecycleMigration_PreserveTraceData(t *testing.T) {
+ g := gomega.NewWithT(t)
+ checkGoroutineLeak(t, g)
+ fileSystem := fs.NewLocalFileSystem()
+
+ // Phase 1: Sender — write known data and flush to disk.
+ senderPath, senderCleanup := mustNewSpace(g)
+ defer senderCleanup()
+
+ senderTst, err := newTSTable(
+ fileSystem, senderPath, common.Position{},
+ logger.GetLogger("sender"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer func() {
+ g.Expect(senderTst.Close()).To(gomega.Succeed())
+ }()
+
+ senderTst.mustAddTraces(tsTS1, nil)
+ time.Sleep(100 * time.Millisecond)
+ senderTst.mustAddTraces(tsTS2, nil)
+ time.Sleep(100 * time.Millisecond)
+
+ waitForFileParts(g, senderTst)
+
+ senderSnp := senderTst.currentSnapshot()
+ g.Expect(senderSnp).NotTo(gomega.BeNil())
+ defer senderSnp.decRef()
+ g.Expect(len(senderSnp.parts)).To(gomega.BeNumerically(">", 0))
+
+ // Phase 2: Receiver — create tsTable and transfer all parts via the real handler.
+ receiverPath, receiverCleanup := mustNewSpace(g)
+ defer receiverCleanup()
+
+ receiverTst, err := newTSTable(
+ fileSystem, receiverPath, common.Position{},
+ logger.GetLogger("receiver"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+
+ transferParts(g, senderSnp, receiverTst, fileSystem, nil)
+
+ // Verify every core file/metadata field round-tripped byte-for-byte.
+ assertMigrationPreservesAllFiles(g, fileSystem, senderSnp, receiverTst, nil)
+
+ // Verify the receiver snapshot has only file-backed parts.
+ recvSnp := receiverTst.currentSnapshot()
+ g.Expect(recvSnp).NotTo(gomega.BeNil())
+ for _, pw := range recvSnp.parts {
+ g.Expect(pw.mp).To(gomega.BeNil(), "receiver should have file-backed parts only")
+ }
+ recvSnp.decRef()
+
+ defer func() {
+ g.Expect(receiverTst.Close()).To(gomega.Succeed())
+ }()
+
+ // Phase 3: Verify all parts on disk are readable via mustOpenFilePart and
+ // query returns correct data from the in-memory snapshot.
+ recvSnp = receiverTst.currentSnapshot()
+ g.Expect(recvSnp).NotTo(gomega.BeNil())
+ defer recvSnp.decRef()
+
+ // Verify each receiver part is well-formed on disk.
+ for _, pw := range recvSnp.parts {
+ p := mustOpenFilePart(pw.p.partMetadata.ID, receiverTst.root, fileSystem)
+ g.Expect(p.partMetadata.TotalCount).To(gomega.BeNumerically(">", uint64(0)), "reopened part must have data")
+ g.Expect(p.primaryBlockMetadata).NotTo(gomega.BeEmpty(), "primary block metadata must be loadable")
+ p.close()
+
+ // Verify metadata.json was written with correct values.
+ pp := partPath(receiverTst.root, pw.p.partMetadata.ID)
+ spd, metaErr := ParsePartMetadata(fileSystem, pp)
+ g.Expect(metaErr).NotTo(gomega.HaveOccurred(), "metadata.json must be readable")
+ g.Expect(spd.TotalCount).To(gomega.BeNumerically(">", uint64(0)), "metadata.json TotalCount must be > 0")
+ g.Expect(spd.MinTimestamp).To(gomega.BeNumerically("<=", spd.MaxTimestamp), "metadata.json timestamps must be valid")
+ }
+
+ // trace1 should return spans from both tsTS1 (span1) and tsTS2 (span4).
+ got := queryTraceID(g, recvSnp, "trace1")
+ g.Expect(got).NotTo(gomega.BeEmpty(), "should have query results for trace1")
+
+ totalSpans := 0
+ for _, tr := range got {
+ totalSpans += len(tr.Spans)
+ }
+ g.Expect(totalSpans).To(gomega.Equal(2), "trace1 should have 2 spans (one from each write batch)")
+
+ // Verify span content: collect all span bytes from all TraceResult entries.
+ var allSpans [][]byte
+ for _, tr := range got {
+ allSpans = append(allSpans, tr.Spans...)
+ }
+ containsSpan := func(spanData [][]byte, target []byte) bool {
+ for _, s := range spanData {
+ if bytes.Equal(s, target) {
+ return true
+ }
+ }
+ return false
+ }
+ g.Expect(containsSpan(allSpans, []byte("span1"))).To(gomega.BeTrue(), "span1 from tsTS1 must be present after migration")
+ g.Expect(containsSpan(allSpans, []byte("span4"))).To(gomega.BeTrue(), "span4 from tsTS2 must be present after migration")
+
+ // Verify tag values: find the Tags slice from any result that contains trace1 data.
+ // The query merges spans from tsTS1 (timestamp=1) and tsTS2 (timestamp=2) into one TraceResult.
+ // Tags are parallel to Spans: Tags[i].Values[j] corresponds to span j.
+ // strTag for trace1: tsTS1="value1", tsTS2="value4".
+ // intTag for trace1: tsTS1=10, tsTS2=40.
+ // strArrTag for trace1: tsTS1=["value1","value2"], tsTS2=["value5","value6"].
+ for _, tr := range got {
+ if len(tr.Tags) == 0 {
+ continue
+ }
+ for _, tag := range tr.Tags {
+ switch tag.Name {
+ case "strTag":
+ for _, tv := range tag.Values {
+ sv := tv.GetStr()
+ g.Expect(sv).NotTo(gomega.BeNil(), "strTag must be a string value")
+ g.Expect(sv.Value == "value1" || sv.Value == "value4").To(gomega.BeTrue(),
+ "strTag value must be value1 (tsTS1) or value4 (tsTS2), got: %s", sv.Value)
+ }
+ case "intTag":
+ for _, tv := range tag.Values {
+ iv := tv.GetInt()
+ g.Expect(iv).NotTo(gomega.BeNil(), "intTag must be an int64 value")
+ g.Expect(iv.Value == int64(10) || iv.Value == int64(40)).To(gomega.BeTrue(),
+ "intTag value must be 10 (tsTS1) or 40 (tsTS2), got: %d", iv.Value)
+ }
+ case "strArrTag":
+ for _, tv := range tag.Values {
+ av := tv.GetStrArray()
+ g.Expect(av).NotTo(gomega.BeNil(), "strArrTag must be a string array value")
+ g.Expect(len(av.Value)).To(gomega.Equal(2), "strArrTag must have 2 elements")
+ }
+ }
+ }
+ }
+
+ // trace2 and trace3 should also be queryable.
+ got2 := queryTraceID(g, recvSnp, "trace2")
+ g.Expect(got2).NotTo(gomega.BeEmpty(), "should have query results for trace2")
+
+ got3 := queryTraceID(g, recvSnp, "trace3")
+ g.Expect(got3).NotTo(gomega.BeEmpty(), "should have query results for trace3")
+}
+
+// TestLifecycleMigration_TwoConcurrentSenders verifies that data from two concurrent senders is preserved correctly.
+func TestLifecycleMigration_TwoConcurrentSenders(t *testing.T) {
+ g := gomega.NewWithT(t)
+ checkGoroutineLeak(t, g)
+ fileSystem := fs.NewLocalFileSystem()
+
+ // Sender 1 writes tsTS1 (timestamps=1), Sender 2 writes tsTS2 (timestamps=2).
+ // Both transfer to the same receiver.
+
+ sender1Path, s1Cleanup := mustNewSpace(g)
+ defer s1Cleanup()
+ sender2Path, s2Cleanup := mustNewSpace(g)
+ defer s2Cleanup()
+
+ mkSender := func(path string, name string, data *traces) (*tsTable, *snapshot) {
+ tst, mkErr := newTSTable(
+ fileSystem, path, common.Position{},
+ logger.GetLogger(name), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(mkErr).NotTo(gomega.HaveOccurred())
+ tst.mustAddTraces(data, nil)
+ time.Sleep(100 * time.Millisecond)
+ waitForFileParts(g, tst)
+ snp := tst.currentSnapshot()
+ g.Expect(snp).NotTo(gomega.BeNil())
+ return tst, snp
+ }
+
+ sender1Tst, sender1Snp := mkSender(sender1Path, "sender1", tsTS1)
+ defer func() {
+ sender1Snp.decRef()
+ g.Expect(sender1Tst.Close()).To(gomega.Succeed())
+ }()
+ sender2Tst, sender2Snp := mkSender(sender2Path, "sender2", tsTS2)
+ defer func() {
+ sender2Snp.decRef()
+ g.Expect(sender2Tst.Close()).To(gomega.Succeed())
+ }()
+
+ receiverPath, receiverCleanup := mustNewSpace(g)
+ defer receiverCleanup()
+
+ receiverTst, err := newTSTable(
+ fileSystem, receiverPath, common.Position{},
+ logger.GetLogger("receiver-two"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer func() {
+ g.Expect(receiverTst.Close()).To(gomega.Succeed())
+ }()
+
+ // Transfer from both senders into the single receiver.
+ transferParts(g, sender1Snp, receiverTst, fileSystem, nil)
+ transferParts(g, sender2Snp, receiverTst, fileSystem, nil)
+
+ // Verify the receiver carries one file-backed part per sender file-backed part across both transfers.
+ expectedParts := 0
+ for _, pw := range sender1Snp.parts {
+ if pw.mp == nil {
+ expectedParts++
+ }
+ }
+ for _, pw := range sender2Snp.parts {
+ if pw.mp == nil {
+ expectedParts++
+ }
+ }
+ recvSnp := receiverTst.currentSnapshot()
+ g.Expect(recvSnp).NotTo(gomega.BeNil())
+ defer recvSnp.decRef()
+ g.Expect(len(recvSnp.parts)).To(gomega.Equal(expectedParts), "receiver part count must equal sum of senders' file-backed parts")
+ for _, pw := range recvSnp.parts {
+ g.Expect(pw.mp).To(gomega.BeNil(), "receiver parts must be file-backed")
+ }
+
+ // trace1 should surface spans from both senders.
+ got := queryTraceID(g, recvSnp, "trace1")
+ g.Expect(got).NotTo(gomega.BeEmpty())
+ totalSpans := 0
+ for _, tr := range got {
+ totalSpans += len(tr.Spans)
+ }
+ g.Expect(totalSpans).To(gomega.Equal(2), "trace1 should have 2 spans from two senders")
+
+ // trace2 should also be present.
+ got2 := queryTraceID(g, recvSnp, "trace2")
+ g.Expect(got2).NotTo(gomega.BeEmpty())
+}
+
+// TestLifecycleMigration_BloomFilterAfterMigration verifies that BloomFilter correctly filters after migration.
+func TestLifecycleMigration_BloomFilterAfterMigration(t *testing.T) {
+ g := gomega.NewWithT(t)
+ checkGoroutineLeak(t, g)
+ fileSystem := fs.NewLocalFileSystem()
+
+ senderPath, senderCleanup := mustNewSpace(g)
+ defer senderCleanup()
+
+ senderTst, err := newTSTable(
+ fileSystem, senderPath, common.Position{},
+ logger.GetLogger("sender-bf"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer func() {
+ g.Expect(senderTst.Close()).To(gomega.Succeed())
+ }()
+
+ senderTst.mustAddTraces(tsTS1, nil)
+ time.Sleep(100 * time.Millisecond)
+
+ waitForFileParts(g, senderTst)
+
+ senderSnp := senderTst.currentSnapshot()
+ g.Expect(senderSnp).NotTo(gomega.BeNil())
+ defer senderSnp.decRef()
+
+ receiverPath, receiverCleanup := mustNewSpace(g)
+ defer receiverCleanup()
+
+ receiverTst, err := newTSTable(
+ fileSystem, receiverPath, common.Position{},
+ logger.GetLogger("receiver-bf"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer func() {
+ g.Expect(receiverTst.Close()).To(gomega.Succeed())
+ }()
+
+ transferParts(g, senderSnp, receiverTst, fileSystem, nil)
+
+ // Verify every core file/metadata field round-tripped byte-for-byte.
+ assertMigrationPreservesAllFiles(g, fileSystem, senderSnp, receiverTst, nil)
+
+ recvSnp := receiverTst.currentSnapshot()
+ g.Expect(recvSnp).NotTo(gomega.BeNil())
+ defer recvSnp.decRef()
+
+ // Existing trace IDs should pass the bloom filter.
+ ppHit, nHit := recvSnp.getParts(nil, 1, 1, []string{"trace1"})
+ g.Expect(nHit).To(gomega.BeNumerically(">", 0), "trace1 should pass BloomFilter")
+ g.Expect(ppHit).NotTo(gomega.BeEmpty())
+
+ // Non-existing trace ID should be rejected by the bloom filter.
+ ppMiss, nMiss := recvSnp.getParts(nil, 1, 1, []string{"nonexistent-trace-id"})
+ g.Expect(nMiss).To(gomega.Equal(0), "nonexistent-trace-id should be filtered by BloomFilter")
+ g.Expect(ppMiss).To(gomega.BeEmpty())
+}
+
+// TestLifecycleMigration_WithSIDX verifies that SIDX parts are correctly migrated alongside core parts.
+// It creates trace data with SIDX (secondary index) entries, transfers both core and SIDX parts to a
+// receiver, and verifies the receiver's SIDX contains the expected file-backed parts.
+func TestLifecycleMigration_WithSIDX(t *testing.T) {
+ g := gomega.NewWithT(t)
+ checkGoroutineLeak(t, g)
+ fileSystem := fs.NewLocalFileSystem()
+
+ const sidxIndexName = "timestamp_millis"
+
+ // Phase 1: Sender — write trace data with SIDX.
+ senderPath, senderCleanup := mustNewSpace(g)
+ defer senderCleanup()
+
+ senderTst, err := newTSTable(
+ fileSystem, senderPath, common.Position{},
+ logger.GetLogger("sender-sidx"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+
+ // Build SIDX write requests that index the traces by timestamp (simulating a timestamp_millis index).
+ sidxReqs1 := []sidx.WriteRequest{
+ {SeriesID: 1, Key: 1, Data: []byte("trace1-ts1")},
+ {SeriesID: 2, Key: 1, Data: []byte("trace2-ts1")},
+ }
+ sidxReqs2 := []sidx.WriteRequest{
+ {SeriesID: 1, Key: 2, Data: []byte("trace1-ts2")},
+ {SeriesID: 3, Key: 2, Data: []byte("trace3-ts2")},
+ }
+
+ // Write first batch with SIDX data.
+ sidxMp1 := mustBuildSidxMemPart(g, senderTst, sidxIndexName, sidxReqs1, 1, 1)
+ senderTst.mustAddTraces(tsTS1, map[string]*sidx.MemPart{sidxIndexName: sidxMp1})
+ time.Sleep(100 * time.Millisecond)
+
+ // Write second batch with SIDX data.
+ sidxMp2 := mustBuildSidxMemPart(g, senderTst, sidxIndexName, sidxReqs2, 2, 2)
+ senderTst.mustAddTraces(tsTS2, map[string]*sidx.MemPart{sidxIndexName: sidxMp2})
+ time.Sleep(100 * time.Millisecond)
+
+ // Wait until core snapshot and SIDX are consistent (both merged to the same part IDs).
+ // waitForConsistentSidxSnapshot returns a snapshot and SIDX paths captured atomically.
+ senderSnp, senderSidxAllPaths := waitForConsistentSidxSnapshot(g, senderTst, []string{sidxIndexName})
+ g.Expect(senderSnp).NotTo(gomega.BeNil())
+ defer senderSnp.decRef()
+ g.Expect(len(senderSnp.parts)).To(gomega.BeNumerically(">", 0))
+ g.Expect(senderSidxAllPaths).To(gomega.HaveKey(sidxIndexName), "sender must have SIDX index %q", sidxIndexName)
+ senderSidxPaths := senderSidxAllPaths[sidxIndexName]
+ g.Expect(senderSidxPaths).NotTo(gomega.BeEmpty(), "sender SIDX must have file-backed parts after flush")
+
+ // Stop the sender's background goroutines (merger, flusher) before transferring.
+ // This prevents the merger from deleting on-disk part directories that transferParts reads.
+ g.Expect(senderTst.Close()).To(gomega.Succeed())
+
+ // Phase 2: Receiver — transfer both core and SIDX parts.
+ receiverPath, receiverCleanup := mustNewSpace(g)
+ defer receiverCleanup()
+
+ receiverTst, err := newTSTable(
+ fileSystem, receiverPath, common.Position{},
+ logger.GetLogger("receiver-sidx"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer func() {
+ g.Expect(receiverTst.Close()).To(gomega.Succeed())
+ }()
+
+ transferParts(g, senderSnp, receiverTst, fileSystem, senderSidxAllPaths)
+
+ // Verify every core and SIDX file/metadata field (including SegmentID) round-tripped byte-for-byte.
+ assertMigrationPreservesAllFiles(g, fileSystem, senderSnp, receiverTst, senderSidxAllPaths)
+
+ // Phase 3: Verify core parts migrated correctly.
+ recvSnp := receiverTst.currentSnapshot()
+ g.Expect(recvSnp).NotTo(gomega.BeNil())
+ defer recvSnp.decRef()
+
+ g.Expect(len(recvSnp.parts)).To(gomega.BeNumerically(">", 0), "receiver must have core parts after migration")
+ for _, pw := range recvSnp.parts {
+ g.Expect(pw.mp).To(gomega.BeNil(), "receiver core parts must be file-backed")
+ }
+
+ // Verify trace data is queryable after migration.
+ got := queryTraceID(g, recvSnp, "trace1")
+ g.Expect(got).NotTo(gomega.BeEmpty(), "trace1 must be queryable after SIDX migration")
+
+ // Phase 4: Verify SIDX parts migrated to receiver.
+ receiverSidxMap := receiverTst.getAllSidx()
+ g.Expect(receiverSidxMap).To(gomega.HaveKey(sidxIndexName), "receiver must have SIDX index %q after migration", sidxIndexName)
+
+ recvPartIDSet := snapshotPartIDSet(recvSnp)
+ receiverSidxPaths := receiverSidxMap[sidxIndexName].PartPaths(recvPartIDSet)
+ g.Expect(receiverSidxPaths).NotTo(gomega.BeEmpty(), "receiver SIDX must have file-backed parts after migration")
+ g.Expect(len(receiverSidxPaths)).To(gomega.Equal(len(senderSidxPaths)),
+ "receiver SIDX part count must match sender SIDX part count")
+
+ // Verify manifest.json in each receiver SIDX part directory.
+ for _, sidxPartPath := range receiverSidxPaths {
+ sidxMetaErr := sidx.ValidatePartMetadata(fileSystem, sidxPartPath)
+ g.Expect(sidxMetaErr).NotTo(gomega.HaveOccurred(), "manifest.json must be valid in SIDX part %s", sidxPartPath)
+ }
+}
+
+// buildSidxTagReqs is a helper for constructing sidx.WriteRequest slices with a string tag value
+// under the fixed tag name "service", suitable for use with mustBuildSidxMemPart when testing
+// tagged SIDX data.
+func buildSidxTagReqs(seriesID uint64, key int64, data []byte, tagValue string) sidx.WriteRequest {
+ return sidx.WriteRequest{
+ SeriesID: common.SeriesID(seriesID),
+ Key: key,
+ Data: data,
+ Tags: []sidx.Tag{
+ {
+ Name: "service",
+ Value: []byte(tagValue),
+ ValueType: pbv1.ValueTypeStr,
+ },
+ },
+ }
+}
+
+// TestLifecycleMigration_WithSIDXTags verifies that SIDX tag data is correctly migrated alongside core parts.
+func TestLifecycleMigration_WithSIDXTags(t *testing.T) {
+ g := gomega.NewWithT(t)
+ checkGoroutineLeak(t, g)
+ fileSystem := fs.NewLocalFileSystem()
+
+ const sidxIndexName = "service_latency"
+
+ // Phase 1: Sender — write trace data with SIDX including tag data.
+ senderPath, senderCleanup := mustNewSpace(g)
+ defer senderCleanup()
+
+ senderTst, err := newTSTable(
+ fileSystem, senderPath, common.Position{},
+ logger.GetLogger("sender-sidx-tags"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+
+ // Build SIDX write requests with tags (simulating indexed span attributes like latency).
+ sidxReqs := []sidx.WriteRequest{
+ buildSidxTagReqs(1, 1, convert.Int64ToBytes(100), "frontend"),
+ buildSidxTagReqs(2, 1, convert.Int64ToBytes(200), "backend"),
+ buildSidxTagReqs(3, 2, convert.Int64ToBytes(150), "cache"),
+ }
+
+ sidxMp := mustBuildSidxMemPart(g, senderTst, sidxIndexName, sidxReqs, 1, 2)
+ senderTst.mustAddTraces(tsTS1, map[string]*sidx.MemPart{sidxIndexName: sidxMp})
+ time.Sleep(100 * time.Millisecond)
+
+ // Wait until core snapshot and SIDX are consistent.
+ senderSnp, senderSidxAllPaths := waitForConsistentSidxSnapshot(g, senderTst, []string{sidxIndexName})
+ g.Expect(senderSnp).NotTo(gomega.BeNil())
+ defer senderSnp.decRef()
+ g.Expect(senderSidxAllPaths).To(gomega.HaveKey(sidxIndexName))
+ senderSidxPaths := senderSidxAllPaths[sidxIndexName]
+ g.Expect(senderSidxPaths).NotTo(gomega.BeEmpty())
+
+ // Stop the sender's background goroutines before transferring to prevent merger from
+ // deleting on-disk part directories that transferParts reads.
+ g.Expect(senderTst.Close()).To(gomega.Succeed())
+
+ // Phase 2: Transfer to receiver.
+ receiverPath, receiverCleanup := mustNewSpace(g)
+ defer receiverCleanup()
+
+ receiverTst, err := newTSTable(
+ fileSystem, receiverPath, common.Position{},
+ logger.GetLogger("receiver-sidx-tags"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+ defer func() {
+ g.Expect(receiverTst.Close()).To(gomega.Succeed())
+ }()
+
+ transferParts(g, senderSnp, receiverTst, fileSystem, senderSidxAllPaths)
+
+ // Verify every core and SIDX file/metadata field (including tag files and SegmentID) round-tripped byte-for-byte.
+ assertMigrationPreservesAllFiles(g, fileSystem, senderSnp, receiverTst, senderSidxAllPaths)
+
+ // Phase 3: Verify receiver has SIDX with tag data.
+ receiverSidxMap := receiverTst.getAllSidx()
+ g.Expect(receiverSidxMap).To(gomega.HaveKey(sidxIndexName), "receiver must have SIDX index %q after migration", sidxIndexName)
+
+ recvSnp := receiverTst.currentSnapshot()
+ g.Expect(recvSnp).NotTo(gomega.BeNil())
+ defer recvSnp.decRef()
+ recvPartIDSet := snapshotPartIDSet(recvSnp)
+ receiverSidxPaths := receiverSidxMap[sidxIndexName].PartPaths(recvPartIDSet)
+ g.Expect(receiverSidxPaths).NotTo(gomega.BeEmpty(), "receiver SIDX must have file-backed parts with tag data")
+ g.Expect(len(receiverSidxPaths)).To(gomega.Equal(len(senderSidxPaths)),
+ "receiver SIDX part count must match sender")
+}
+
+// TestLifecycleMigration_ReceiverSurvivesRestart verifies that file-backed parts and SIDX parts
+// introduced via migration survive a receiver restart. This guards the snapshot-persist invariant:
+// introducePart must persist the snapshot for file-backed introductions, otherwise loadSnapshot
+// would treat the on-disk part as orphan and gc.removePart would delete it.
+func TestLifecycleMigration_ReceiverSurvivesRestart(t *testing.T) {
+ g := gomega.NewWithT(t)
+ checkGoroutineLeak(t, g)
+ fileSystem := fs.NewLocalFileSystem()
+
+ const sidxIndexName = "service_latency_restart"
+
+ // Phase 1: Sender writes core trace data + SIDX with non-zero SegmentID and tag data.
+ senderPath, senderCleanup := mustNewSpace(g)
+ defer senderCleanup()
+
+ senderTst, err := newTSTable(
+ fileSystem, senderPath, common.Position{},
+ logger.GetLogger("sender-restart"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+
+ sidxReqs := []sidx.WriteRequest{
+ buildSidxTagReqs(1, 1, convert.Int64ToBytes(100), "frontend"),
+ buildSidxTagReqs(2, 2, convert.Int64ToBytes(200), "backend"),
+ }
+ sidxMp := mustBuildSidxMemPart(g, senderTst, sidxIndexName, sidxReqs, 1, 2)
+ senderTst.mustAddTraces(tsTS1, map[string]*sidx.MemPart{sidxIndexName: sidxMp})
+ time.Sleep(100 * time.Millisecond)
+
+ senderSnp, senderSidxAllPaths := waitForConsistentSidxSnapshot(g, senderTst, []string{sidxIndexName})
+ g.Expect(senderSnp).NotTo(gomega.BeNil())
+ defer senderSnp.decRef()
+ g.Expect(senderSidxAllPaths).To(gomega.HaveKey(sidxIndexName))
+
+ g.Expect(senderTst.Close()).To(gomega.Succeed())
+
+ // Phase 2: Receiver receives the migration.
+ receiverPath, receiverCleanup := mustNewSpace(g)
+ defer receiverCleanup()
+
+ receiverTst, err := newTSTable(
+ fileSystem, receiverPath, common.Position{},
+ logger.GetLogger("receiver-restart-1"), timestamp.TimeRange{},
+ option{flushTimeout: 0, mergePolicy: newDefaultMergePolicyForTesting(), protector: protector.Nop{}},
+ nil,
+ )
+ g.Expect(err).NotTo(gomega.HaveOccurred())
+
+ transferParts(g, senderSnp, receiverTst, fileSystem, senderSidxAllPaths)
+ assertMigrationPreservesAllFiles(g, fileSystem, senderSnp, receiverTst, senderSidxAllPaths)
+
+ // Capture pre-restart state for comparison.
+ preRestartSnp := receiverTst.currentSnapshot()
+ g.Expect(preRestartSnp).NotTo(gomega.BeNil())
+ preRestartCorePartIDs := snapshotPartIDSet(preRestartSnp)
+ g.Expect(preRestartCorePartIDs).NotTo(gomega.BeEmpty(), "pre-restart receiver must have file-backed parts")
+
+ preRestartSidxMap := receiverTst.getAllSidx()
+ preRestartSidxPaths := preRestartSidxMap[sidxIndexName].PartPaths(preRestartCorePartIDs)
+ g.Expect(preRestartSidxPaths).NotTo(gomega.BeEmpty(), "pre-restart receiver SIDX must have file-backed parts")
+ preRestartSnp.decRef()
+
+ preRestartGot := func() []model.TraceResult {
+ s := receiverTst.currentSnapshot()
+ defer s.decRef()
+ return queryTraceID(g, s, "trace1")
+ }()
+ g.Expect(preRestartGot).NotTo(gomega.BeEmpty(), "trace1 must be queryable before restart")
+
+ // Phase 3: Restart — close and reopen the receiver tsTable at the same root.
+ reopenedTst := reopenTSTable(g, receiverTst, fileSystem, receiverPath, "receiver-restart-2")
+ defer func() {
+ g.Expect(reopenedTst.Close()).To(gomega.Succeed())
+ }()
+
+ // Verify core parts survived: same partIDs, all file-backed.
+ postRestartSnp := reopenedTst.currentSnapshot()
+ g.Expect(postRestartSnp).NotTo(gomega.BeNil(), "reopened receiver must have a snapshot")
+ defer postRestartSnp.decRef()
+ postRestartCorePartIDs := snapshotPartIDSet(postRestartSnp)
+ g.Expect(postRestartCorePartIDs).To(gomega.Equal(preRestartCorePartIDs),
+ "reopened core part IDs must match pre-restart (snapshot must have been persisted)")
+ for _, pw := range postRestartSnp.parts {
+ g.Expect(pw.mp).To(gomega.BeNil(), "reopened parts must be file-backed")
+ }
+
+ // Verify SIDX survived: same partIDs and on-disk paths still exist.
+ postRestartSidxMap := reopenedTst.getAllSidx()
+ g.Expect(postRestartSidxMap).To(gomega.HaveKey(sidxIndexName), "SIDX index must reopen")
+ postRestartSidxPaths := postRestartSidxMap[sidxIndexName].PartPaths(postRestartCorePartIDs)
+ g.Expect(postRestartSidxPaths).To(gomega.Equal(preRestartSidxPaths),
+ "reopened SIDX part paths must match pre-restart (SIDX snapshot must persist as well)")
+
+ // Verify queries still return data on the reopened tsTable.
+ postRestartGot := queryTraceID(g, postRestartSnp, "trace1")
+ g.Expect(postRestartGot).NotTo(gomega.BeEmpty(), "trace1 must remain queryable after restart")
+
+ preSpans := 0
+ for _, tr := range preRestartGot {
+ preSpans += len(tr.Spans)
+ }
+ postSpans := 0
+ for _, tr := range postRestartGot {
+ postSpans += len(tr.Spans)
+ }
+ g.Expect(postSpans).To(gomega.Equal(preSpans), "span count must be identical after restart")
+}
diff --git a/banyand/trace/part_metadata.go b/banyand/trace/part_metadata.go
index bd3aec7a5..ae1a029a2 100644
--- a/banyand/trace/part_metadata.go
+++ b/banyand/trace/part_metadata.go
@@ -54,6 +54,15 @@ func (pm *partMetadata) reset() {
pm.ID = 0
}
+func (pm *partMetadata) fillFromSyncContext(ctx *queue.ChunkedSyncPartContext) {
+ pm.CompressedSizeBytes = ctx.CompressedSizeBytes
+ pm.UncompressedSpanSizeBytes = ctx.UncompressedSizeBytes
+ pm.TotalCount = ctx.TotalCount
+ pm.BlocksCount = ctx.BlocksCount
+ pm.MinTimestamp = ctx.MinTimestamp
+ pm.MaxTimestamp = ctx.MaxTimestamp
+}
+
func validatePartMetadata(fileSystem fs.FileSystem, partPath string) error {
metadataPath := filepath.Join(partPath, metadataFilename)
metadata, err := fileSystem.Read(metadataPath)
diff --git a/banyand/trace/streaming_pipeline_test.go b/banyand/trace/streaming_pipeline_test.go
index f1435c246..0d7dab1ae 100644
--- a/banyand/trace/streaming_pipeline_test.go
+++ b/banyand/trace/streaming_pipeline_test.go
@@ -90,6 +90,10 @@ func (f *fakeSIDX) PrepareMemPart(uint64, *sidx.MemPart) func(cur *sidx.Snapshot
return func(cur *sidx.Snapshot) *sidx.Snapshot { return cur }
}
+func (f *fakeSIDX) PrepareFilePart(uint64, string) func(cur *sidx.Snapshot) *sidx.Snapshot {
+ return func(cur *sidx.Snapshot) *sidx.Snapshot { return cur }
+}
+
func (f *fakeSIDX) PrepareFlushed(*sidx.FlusherIntroduction) func(cur *sidx.Snapshot) *sidx.Snapshot {
return func(cur *sidx.Snapshot) *sidx.Snapshot { return cur }
}
@@ -684,6 +688,10 @@ func (f *fakeSIDXInfinite) PrepareMemPart(uint64, *sidx.MemPart) func(cur *sidx.
return func(cur *sidx.Snapshot) *sidx.Snapshot { return cur }
}
+func (f *fakeSIDXInfinite) PrepareFilePart(uint64, string) func(cur *sidx.Snapshot) *sidx.Snapshot {
+ return func(cur *sidx.Snapshot) *sidx.Snapshot { return cur }
+}
+
func (f *fakeSIDXInfinite) PrepareFlushed(*sidx.FlusherIntroduction) func(cur *sidx.Snapshot) *sidx.Snapshot {
return func(cur *sidx.Snapshot) *sidx.Snapshot { return cur }
}
diff --git a/banyand/trace/tstable.go b/banyand/trace/tstable.go
index 741f1221f..05d5e11d1 100644
--- a/banyand/trace/tstable.go
+++ b/banyand/trace/tstable.go
@@ -502,14 +502,36 @@ func (tst *tsTable) Close() error {
return tst.closeSidxMap()
}
+func (tst *tsTable) mustAddFilePart(partID uint64, sidxFilePartsMap map[string]string) {
+ p := mustOpenFilePart(partID, tst.root, tst.fileSystem)
+ p.partMetadata.ID = partID
+
+ ind := generateIntroduction()
+ defer releaseIntroduction(ind)
+ ind.applied = make(chan struct{})
+ ind.part = newPartWrapper(nil, p)
+ ind.sidxFilePartsMap = sidxFilePartsMap
+
+ select {
+ case tst.introductions <- ind:
+ case <-tst.loopCloser.CloseNotify():
+ ind.part.decRef()
+ return
+ }
+ select {
+ case <-ind.applied:
+ case <-tst.loopCloser.CloseNotify():
+ }
+}
+
func (tst *tsTable) mustAddMemPart(mp *memPart, sidxReqsMap map[string]*sidx.MemPart) {
p := openMemPart(mp)
ind := generateIntroduction()
defer releaseIntroduction(ind)
ind.applied = make(chan struct{})
- ind.memPart = newPartWrapper(mp, p)
- ind.memPart.p.partMetadata.ID = atomic.AddUint64(&tst.curPartID, 1)
+ ind.part = newPartWrapper(mp, p)
+ ind.part.p.partMetadata.ID = atomic.AddUint64(&tst.curPartID, 1)
ind.sidxReqsMap = sidxReqsMap
startTime := time.Now()
totalCount := mp.partMetadata.TotalCount
@@ -518,7 +540,7 @@ func (tst *tsTable) mustAddMemPart(mp *memPart, sidxReqsMap map[string]*sidx.Mem
case tst.introductions <- ind:
case <-tst.loopCloser.CloseNotify():
tst.addPendingDataCount(-int64(totalCount))
- ind.memPart.decRef()
+ ind.part.decRef()
return
}
select {
diff --git a/banyand/trace/write_data.go b/banyand/trace/write_data.go
index c7290c349..5c6e6d302 100644
--- a/banyand/trace/write_data.go
+++ b/banyand/trace/write_data.go
@@ -19,7 +19,9 @@ package trace
import (
"fmt"
+ "path/filepath"
"strings"
+ "sync/atomic"
"time"
"github.com/apache/skywalking-banyandb/api/common"
@@ -27,28 +29,41 @@ import (
"github.com/apache/skywalking-banyandb/banyand/internal/sidx"
"github.com/apache/skywalking-banyandb/banyand/internal/storage"
"github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
"github.com/apache/skywalking-banyandb/pkg/index"
"github.com/apache/skywalking-banyandb/pkg/logger"
)
type syncPartContext struct {
tsTable *tsTable
+ fileSystem fs.FileSystem
l *logger.Logger
writers *writers
- memPart *memPart
+ partPath string
sidxPartContexts map[string]*sidx.SyncPartContext
traceIDFilterBuffer []byte
tagTypeBuffer []byte
+ partMeta partMetadata
+ partID uint64
}
func (s *syncPartContext) NewPartType(ctx *queue.ChunkedSyncPartContext) error {
if ctx.PartType != PartTypeCore {
+ // Allocate partID on first call so SIDX parts use the same ID as the core part.
+ if s.partID == 0 {
+ s.partID = atomic.AddUint64(&s.tsTable.curPartID, 1)
+ s.fileSystem = s.tsTable.fileSystem
+ }
+ // Pre-register the SIDX instance before writing files so its init/loadSnapshot
+ // runs against an empty directory. Otherwise mustGetOrCreateSidx in introducePart
+ // would lazily create the SIDX, scan the directory, and delete the freshly-written
+ // part because it isn't in the (nil) availablePartIDs list.
+ if _, err := s.tsTable.getOrCreateSidx(ctx.PartType); err != nil {
+ return fmt.Errorf("cannot pre-create sidx %q: %w", ctx.PartType, err)
+ }
sidxPartContext := sidx.NewSyncPartContext()
- memPart := sidx.GenerateMemPart()
- memPart.SetPartMetadata(ctx.CompressedSizeBytes, ctx.UncompressedSizeBytes, ctx.TotalCount, ctx.BlocksCount, ctx.MinKey, ctx.MaxKey, ctx.ID)
- writers := sidx.GenerateWriters()
- writers.MustInitForMemPart(memPart)
- sidxPartContext.Set(ctx.PartType, memPart, writers)
+ sidxPartPath := filepath.Join(s.tsTable.root, sidxDirName, ctx.PartType, fmt.Sprintf("%016x", s.partID))
+ sidxPartContext.SetForFile(ctx.PartType, s.fileSystem, sidxPartPath, ctx, s.tsTable.pm.ShouldCache(int64(ctx.CompressedSizeBytes)))
if s.sidxPartContexts == nil {
s.sidxPartContexts = make(map[string]*sidx.SyncPartContext)
}
@@ -56,59 +71,68 @@ func (s *syncPartContext) NewPartType(ctx *queue.ChunkedSyncPartContext) error {
return nil
}
- memPart := generateMemPart()
- memPart.partMetadata.CompressedSizeBytes = ctx.CompressedSizeBytes
- memPart.partMetadata.UncompressedSpanSizeBytes = ctx.UncompressedSizeBytes
- memPart.partMetadata.TotalCount = ctx.TotalCount
- memPart.partMetadata.BlocksCount = ctx.BlocksCount
- memPart.partMetadata.MinTimestamp = ctx.MinTimestamp
- memPart.partMetadata.MaxTimestamp = ctx.MaxTimestamp
- memPart.partMetadata.ID = ctx.ID
- writers := generateWriters()
- writers.mustInitForMemPart(memPart)
- s.writers = writers
- s.memPart = memPart
+ // Allocate partID if not already allocated by an earlier SIDX NewPartType call.
+ if s.partID == 0 {
+ s.partID = atomic.AddUint64(&s.tsTable.curPartID, 1)
+ }
+ s.partPath = partPath(s.tsTable.root, s.partID)
+ s.fileSystem = s.tsTable.fileSystem
+
+ w := generateWriters()
+ w.mustInitForFilePart(s.fileSystem, s.partPath, s.tsTable.pm.ShouldCache(int64(ctx.CompressedSizeBytes)))
+ s.writers = w
+
+ s.partMeta.fillFromSyncContext(ctx)
return nil
}
func (s *syncPartContext) FinishSync() error {
- if len(s.traceIDFilterBuffer) > 0 && s.memPart != nil {
- bf := generateTraceIDBloomFilter()
- s.memPart.traceIDFilter.filter = decodeBloomFilter(s.traceIDFilterBuffer, bf)
+ if s.partPath == "" {
+ // No core part was written (only SIDX parts arrived). Nothing to finalize;
+ // Close will discard any partially-written SIDX parts too.
+ return s.Close()
}
- if len(s.tagTypeBuffer) > 0 && s.memPart != nil {
- if s.memPart.tagType == nil {
- s.memPart.tagType = make(tagType)
- }
- if err := s.memPart.tagType.unmarshal(s.tagTypeBuffer); err != nil {
- s.l.Error().Err(err).Msg("failed to unmarshal tag type data")
- return err
- }
+
+ // Close writers first so file data is flushed before we write sidecar metadata.
+ s.releaseCoreWriters()
+
+ if len(s.traceIDFilterBuffer) > 0 {
+ fs.MustFlush(s.fileSystem, s.traceIDFilterBuffer, filepath.Join(s.partPath, traceIDFilterFilename), storage.FilePerm)
+ }
+ if len(s.tagTypeBuffer) > 0 {
+ fs.MustFlush(s.fileSystem, s.tagTypeBuffer, filepath.Join(s.partPath, tagTypeFilename), storage.FilePerm)
}
+ s.partMeta.mustWriteMetadata(s.fileSystem, s.partPath)
+ s.fileSystem.SyncPath(s.partPath)
- if s.memPart != nil {
- mp := s.memPart
- s.memPart = nil
- sidxPartContexts := make(map[string]*sidx.MemPart, len(s.sidxPartContexts))
- for _, sidxPartContext := range s.sidxPartContexts {
- sidxPartContexts[sidxPartContext.Name()] = sidxPartContext.GetMemPart()
- }
- s.tsTable.mustAddMemPart(mp, sidxPartContexts)
+ // Finish SIDX writers and collect file paths for file-backed parts.
+ sidxFilePartsMap := make(map[string]string, len(s.sidxPartContexts))
+ for _, sidxPartContext := range s.sidxPartContexts {
+ sidxFilePartsMap[sidxPartContext.Name()] = sidxPartContext.Finish()
}
+ s.tsTable.mustAddFilePart(s.partID, sidxFilePartsMap)
+ s.partPath = ""
+ s.traceIDFilterBuffer = nil
+ s.tagTypeBuffer = nil
return s.Close()
}
-func (s *syncPartContext) Close() error {
- if s.writers != nil {
- s.writers.MustClose()
- releaseWriters(s.writers)
- s.writers = nil
+// releaseCoreWriters closes and releases the core writers, if any. Safe to call multiple times.
+func (s *syncPartContext) releaseCoreWriters() {
+ if s.writers == nil {
+ return
}
- if s.memPart != nil {
- // syncPartContext owns the memPart directly without partWrapper refcounting.
- // It must release via releaseMemPart, not decRef, which is used in mustAddMemPart.
- releaseMemPart(s.memPart)
- s.memPart = nil
+ s.writers.MustClose()
+ releaseWriters(s.writers)
+ s.writers = nil
+}
+
+func (s *syncPartContext) Close() error {
+ s.releaseCoreWriters()
+ // Clean up incomplete filePart on error (partPath is cleared on success in FinishSync)
+ if s.partPath != "" && s.fileSystem != nil {
+ s.fileSystem.MustRMAll(s.partPath)
+ s.partPath = ""
}
if s.sidxPartContexts != nil {
for _, sidxPartContext := range s.sidxPartContexts {
@@ -117,6 +141,7 @@ func (s *syncPartContext) Close() error {
s.sidxPartContexts = nil
}
s.tsTable = nil
+ s.fileSystem = nil
s.traceIDFilterBuffer = nil
s.tagTypeBuffer = nil
return nil
@@ -329,13 +354,9 @@ func (s *syncChunkCallback) handleTraceFileChunk(ctx *queue.ChunkedSyncPartConte
case fileName == traceSpansName:
partCtx.writers.spanWriter.MustWrite(chunk)
case fileName == traceIDFilterFilename:
- if partCtx.memPart != nil {
- s.handleTraceIDFilterChunk(partCtx, chunk)
- }
+ s.handleTraceIDFilterChunk(partCtx, chunk)
case fileName == tagTypeFilename:
- if partCtx.memPart != nil {
- s.handleTagTypeChunk(partCtx, chunk)
- }
+ s.handleTagTypeChunk(partCtx, chunk)
case strings.HasPrefix(fileName, traceTagsPrefix):
tagName := fileName[len(traceTagsPrefix):]
_, tagWriter := partCtx.writers.getWriters(tagName)
(skywalking-banyandb) 14/24: Fix nil pointer panic in disk monitor during early initialization (#1082)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 37dcb4f3c8f0ae116925de96b1d1edaad76d8bcc
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Tue Apr 21 20:21:38 2026 +0800
Fix nil pointer panic in disk monitor during early initialization (#1082)
* Fix nil pointer panic in disk monitor during early initialization
The disk monitor's monitorLoop could panic with "invalid memory address
or nil pointer dereference" when groups are registered in the schema
repo but their schema hasn't been populated yet (race between group
creation and async schema loading).
Two fixes:
- Nil-check group.GetSchema() in findGroupWithOldestSegment before
accessing Metadata.Name to prevent the panic
- Move panic recovery from the outer loop into per-tick scope so a
recovered panic doesn't kill the monitor goroutine permanently
* Address review feedback: reset isActive on panic, add stack trace, add nil-schema tests
- Reset dm.isActive and metric gauge in panic recovery to prevent
Stop() from hanging indefinitely if a panic occurs mid-cleanup
- Log stack trace via runtime/debug.Stack() alongside the panic
value for easier diagnosis of future panics
- Add unit tests for groups with nil schema in findGroupWithOldestSegment
to cover the exact scenario that caused the production panic
---------
Co-authored-by: Huang Youliang <bu...@gmail.com>
---
banyand/internal/storage/disk_monitor.go | 27 ++++++++++++------
banyand/internal/storage/disk_monitor_test.go | 41 ++++++++++++++++++++++-----
2 files changed, 53 insertions(+), 15 deletions(-)
diff --git a/banyand/internal/storage/disk_monitor.go b/banyand/internal/storage/disk_monitor.go
index bcaa8550d..58a8debde 100644
--- a/banyand/internal/storage/disk_monitor.go
+++ b/banyand/internal/storage/disk_monitor.go
@@ -19,6 +19,7 @@ package storage
import (
"os"
+ "runtime/debug"
"sort"
"sync/atomic"
"time"
@@ -177,22 +178,28 @@ func (dm *DiskMonitor) Stop() {
}
func (dm *DiskMonitor) monitorLoop(serviceName string) {
- defer func() {
- if r := recover(); r != nil {
- dm.logger.Error().Interface("panic", r).Msg("disk monitor panic recovered")
- }
- }()
-
for {
select {
case <-dm.stopCh:
return
case <-dm.ticker.C:
- dm.checkAndCleanup(serviceName)
+ dm.safeCheckAndCleanup(serviceName)
}
}
}
+func (dm *DiskMonitor) safeCheckAndCleanup(serviceName string) {
+ defer func() {
+ if r := recover(); r != nil {
+ dm.logger.Error().Interface("panic", r).Str("stack", string(debug.Stack())).Msg("disk monitor panic recovered")
+ // Reset active state so Stop() doesn't hang and the next tick can retry
+ dm.isActive.Store(false)
+ dm.metrics.forcedRetentionActive.Set(0, serviceName)
+ }
+ }()
+ dm.checkAndCleanup(serviceName)
+}
+
func (dm *DiskMonitor) checkAndCleanup(serviceName string) {
// Check disk usage using real-time calculation for responsive forced cleanup
diskPercent, err := getRealTimeDiskUsagePercent(dm.service.GetDataPath())
@@ -344,7 +351,11 @@ func (dm *DiskMonitor) findGroupWithOldestSegment(groups []resourceSchema.Group)
// Query each group's oldest segment end time
for _, group := range groups {
- groupName := group.GetSchema().Metadata.Name
+ schema := group.GetSchema()
+ if schema == nil || schema.Metadata == nil {
+ continue
+ }
+ groupName := schema.Metadata.Name
endTime, hasSegments := dm.service.PeekOldestSegmentEndTimeInGroup(groupName)
// Only consider groups that have segments
diff --git a/banyand/internal/storage/disk_monitor_test.go b/banyand/internal/storage/disk_monitor_test.go
index c66e510eb..65fd8f77c 100644
--- a/banyand/internal/storage/disk_monitor_test.go
+++ b/banyand/internal/storage/disk_monitor_test.go
@@ -168,15 +168,11 @@ func (m *mockHistogram) Delete(_ ...string) bool {
// Mock group implementation.
type mockGroup struct {
- name string
+ schema *commonv1.Group
}
func (m *mockGroup) GetSchema() *commonv1.Group {
- return &commonv1.Group{
- Metadata: &commonv1.Metadata{
- Name: m.name,
- },
- }
+ return m.schema
}
func (m *mockGroup) SupplyTSDB() io.Closer {
@@ -184,7 +180,17 @@ func (m *mockGroup) SupplyTSDB() io.Closer {
}
func createMockGroup(name string) resourceSchema.Group {
- return &mockGroup{name: name}
+ return &mockGroup{
+ schema: &commonv1.Group{
+ Metadata: &commonv1.Metadata{
+ Name: name,
+ },
+ },
+ }
+}
+
+func createMockGroupWithNilSchema() resourceSchema.Group {
+ return &mockGroup{schema: nil}
}
func TestNewDiskMonitor(t *testing.T) {
@@ -314,6 +320,27 @@ func TestDiskMonitor_findGroupWithOldestSegment(t *testing.T) {
assert.Equal(t, "group2", result)
})
+ t.Run("groups with nil schema are skipped", func(t *testing.T) {
+ nilGroup := createMockGroupWithNilSchema()
+ group1 := createMockGroup("group1")
+ groups := []resourceSchema.Group{nilGroup, group1}
+
+ now := time.Now()
+ service.SetSegmentTime("group1", now.Add(-time.Hour), true)
+
+ result := dm.findGroupWithOldestSegment(groups)
+ assert.Equal(t, "group1", result)
+ })
+
+ t.Run("all groups with nil schema returns empty", func(t *testing.T) {
+ nilGroup1 := createMockGroupWithNilSchema()
+ nilGroup2 := createMockGroupWithNilSchema()
+ groups := []resourceSchema.Group{nilGroup1, nilGroup2}
+
+ result := dm.findGroupWithOldestSegment(groups)
+ assert.Empty(t, result)
+ })
+
t.Run("mixed groups - some with segments, some without", func(t *testing.T) {
group1 := createMockGroup("group1")
group2 := createMockGroup("group2")
(skywalking-banyandb) 10/24: Fix wrong backup path of schema property (#1070)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 1624c5903270603183541605fb8eae4586a20eaa
Author: mrproliu <74...@qq.com>
AuthorDate: Wed Apr 15 13:46:32 2026 +0800
Fix wrong backup path of schema property (#1070)
---
banyand/backup/backup_test.go | 9 ++++++++-
banyand/backup/snapshot/snapshot.go | 10 +++++++++-
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/banyand/backup/backup_test.go b/banyand/backup/backup_test.go
index f02cd7c96..e2227057b 100644
--- a/banyand/backup/backup_test.go
+++ b/banyand/backup/backup_test.go
@@ -79,11 +79,18 @@ func TestGetSnapshotDir(t *testing.T) {
filepath.Join("/tmp/trace", storage.SnapshotsDir, "test"),
false,
},
+ {
+ "property catalog",
+ &databasev1.Snapshot{Catalog: commonv1.Catalog_CATALOG_PROPERTY, Name: "test"},
+ "/tmp", "/tmp", "/tmp", "/tmp", "/tmp",
+ filepath.Join("/tmp/property", storage.SnapshotsDir, "test", storage.DataDir),
+ false,
+ },
{
"schema-property catalog",
&databasev1.Snapshot{Catalog: commonv1.Catalog_CATALOG_PROPERTY, Name: "schema-property/test"},
"/tmp", "/tmp", "/tmp", "/tmp", "/tmp",
- filepath.Join("/tmp/schema-property", storage.SnapshotsDir, "test"),
+ filepath.Join("/tmp/schema-property", storage.SnapshotsDir, "test", storage.DataDir),
false,
},
{
diff --git a/banyand/backup/snapshot/snapshot.go b/banyand/backup/snapshot/snapshot.go
index dcc7522ff..cadc3d4bc 100644
--- a/banyand/backup/snapshot/snapshot.go
+++ b/banyand/backup/snapshot/snapshot.go
@@ -72,9 +72,13 @@ func Dir(snapshot *databasev1.Snapshot, streamRoot, measureRoot, propertyRoot, t
if strings.HasPrefix(snapshot.Name, SchemaPropertyCatalogName+"/") {
actualName := strings.TrimPrefix(snapshot.Name, SchemaPropertyCatalogName+"/")
baseDir := filepath.Join(schemaRoot, SchemaPropertyCatalogName)
- return filepath.Join(baseDir, storage.SnapshotsDir, actualName), nil
+ return filepath.Join(baseDir, storage.SnapshotsDir, actualName, storage.DataDir), nil
}
var baseDir string
+ // Property DB writes its snapshot contents under <snapshotDir>/<sn>/data/<group>/<shard>/.
+ // Point backup at the inner data/ to keep remote paths aligned with the other catalogs
+ // and prevent restore from producing <property>/data/data/... nesting.
+ propertyDataDir := false
switch snapshot.Catalog {
case commonv1.Catalog_CATALOG_STREAM:
baseDir = LocalDir(streamRoot, snapshot.Catalog)
@@ -82,11 +86,15 @@ func Dir(snapshot *databasev1.Snapshot, streamRoot, measureRoot, propertyRoot, t
baseDir = LocalDir(measureRoot, snapshot.Catalog)
case commonv1.Catalog_CATALOG_PROPERTY:
baseDir = LocalDir(propertyRoot, snapshot.Catalog)
+ propertyDataDir = true
case commonv1.Catalog_CATALOG_TRACE:
baseDir = LocalDir(traceRoot, snapshot.Catalog)
default:
return "", errors.New("unknown catalog type")
}
+ if propertyDataDir {
+ return filepath.Join(baseDir, storage.SnapshotsDir, snapshot.Name, storage.DataDir), nil
+ }
return filepath.Join(baseDir, storage.SnapshotsDir, snapshot.Name), nil
}
(skywalking-banyandb) 01/24: Fix the reuse of byte arrays in the underlying min/max implementation (#1022)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit f822dcdc13fda679d6aa815b95e21d7fd23736d3
Author: eye-gu <73...@qq.com>
AuthorDate: Fri Mar 27 10:32:02 2026 +0800
Fix the reuse of byte arrays in the underlying min/max implementation (#1022)
---
banyand/stream/tag_filter_test.go | 44 +++++++++++++++++++++++++++++++++++++++
banyand/stream/tag_metadata.go | 8 +++++++
2 files changed, 52 insertions(+)
diff --git a/banyand/stream/tag_filter_test.go b/banyand/stream/tag_filter_test.go
index 363b1c621..a2bde72d8 100644
--- a/banyand/stream/tag_filter_test.go
+++ b/banyand/stream/tag_filter_test.go
@@ -472,6 +472,50 @@ func BenchmarkTagFamilyFiltersUnmarshal(b *testing.B) {
}
}
+func TestTagFamilyFilterUnmarshalMinMaxCorruption(t *testing.T) {
+ const (
+ minValue int64 = 100
+ maxValue int64 = 200
+ )
+
+ tfm := generateTagFamilyMetadata()
+ defer releaseTagFamilyMetadata(tfm)
+
+ bf := filter.NewBloomFilter(10)
+ bf.Add([]byte("test-value"))
+ filterBuf := make([]byte, 0)
+ filterBuf = encodeBloomFilter(filterBuf, bf)
+
+ tfm.tagMetadata = append(tfm.tagMetadata, tagMetadata{
+ name: "numeric-tag",
+ valueType: pbv1.ValueTypeInt64,
+ min: convert.Int64ToBytes(minValue),
+ max: convert.Int64ToBytes(maxValue),
+ filterBlock: dataBlock{
+ offset: 0,
+ size: uint64(len(filterBuf)),
+ },
+ })
+
+ metaBuf := make([]byte, 0)
+ metaBuf = tfm.marshal(metaBuf)
+
+ tff := generateTagFamilyFilter()
+ defer releaseTagFamilyFilter(tff)
+ tff.unmarshal(
+ &dataBlock{offset: 0, size: uint64(len(metaBuf))},
+ &mockReader{data: metaBuf},
+ &mockReader{data: filterBuf},
+ &mockReader{data: []byte{}},
+ )
+
+ tagFilter, ok := (*tff)["numeric-tag"]
+ if assert.True(t, ok, "tag filter for numeric-tag should exist") && assert.NotNil(t, tagFilter, "tag filter for numeric-tag should not be nil") {
+ assert.Equal(t, minValue, convert.BytesToInt64(tagFilter.min), "min value should not be corrupted by bb.Buf reuse")
+ assert.Equal(t, maxValue, convert.BytesToInt64(tagFilter.max), "max value should not be corrupted by bb.Buf reuse")
+ }
+}
+
func generateMetaAndFilterWithScenarios(scenarios []struct {
name string
valueType pbv1.ValueType
diff --git a/banyand/stream/tag_metadata.go b/banyand/stream/tag_metadata.go
index b30aa766a..be0d7bf17 100644
--- a/banyand/stream/tag_metadata.go
+++ b/banyand/stream/tag_metadata.go
@@ -18,6 +18,7 @@
package stream
import (
+ "bytes"
"fmt"
"github.com/apache/skywalking-banyandb/pkg/convert"
@@ -79,10 +80,17 @@ func (tm *tagMetadata) unmarshal(src []byte) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("cannot unmarshal tagMetadata.min: %w", err)
}
+ tm.min = bytes.Clone(tm.min)
+ if len(tm.min) > 0 {
+ tm.min = bytes.Clone(tm.min)
+ }
src, tm.max, err = encoding.DecodeBytes(src)
if err != nil {
return nil, fmt.Errorf("cannot unmarshal tagMetadata.max: %w", err)
}
+ if len(tm.max) > 0 {
+ tm.max = bytes.Clone(tm.max)
+ }
src = tm.filterBlock.unmarshal(src)
return src, nil
}
(skywalking-banyandb) 16/24: fix(topn): deduplicate entities in TopN aggregation query (#1085)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit af4c71362478c143f6ce309f584059e1042e3d08
Author: eye-gu <73...@qq.com>
AuthorDate: Thu Apr 23 19:03:25 2026 +0800
fix(topn): deduplicate entities in TopN aggregation query (#1085)
---
banyand/query/processor_topn.go | 42 +++-
pkg/query/logical/measure/topn_analyzer.go | 18 +-
pkg/query/logical/measure/topn_plan_distinct.go | 268 +++++++++++++++++++++
.../logical/measure/topn_plan_distinct_test.go | 173 +++++++++++++
.../service_instance_metric_topn_test.json | 41 ++++
.../testdata/topn_aggregations/topn_agg_test.json | 17 ++
test/cases/init.go | 1 +
.../service_instance_metric_topn_test_data.json | 152 ++++++++++++
test/cases/topn/data/data.go | 91 +++++++
test/cases/topn/data/input/topn_count.ql | 22 ++
test/cases/topn/data/input/topn_count.yaml | 22 ++
test/cases/topn/data/input/topn_max.ql | 22 ++
test/cases/topn/data/input/topn_max.yaml | 22 ++
test/cases/topn/data/input/topn_mean.ql | 22 ++
test/cases/topn/data/input/topn_mean.yaml | 22 ++
test/cases/topn/data/input/topn_min.ql | 22 ++
test/cases/topn/data/input/topn_min.yaml | 22 ++
test/cases/topn/data/input/topn_sum.ql | 22 ++
test/cases/topn/data/input/topn_sum.yaml | 22 ++
test/cases/topn/data/want/topn_count.yaml | 43 ++++
test/cases/topn/data/want/topn_max.yaml | 43 ++++
test/cases/topn/data/want/topn_mean.yaml | 35 +++
test/cases/topn/data/want/topn_min.yaml | 43 ++++
test/cases/topn/data/want/topn_sum.yaml | 43 ++++
test/cases/topn/topn.go | 16 +-
25 files changed, 1226 insertions(+), 20 deletions(-)
diff --git a/banyand/query/processor_topn.go b/banyand/query/processor_topn.go
index 33556434c..25c360013 100644
--- a/banyand/query/processor_topn.go
+++ b/banyand/query/processor_topn.go
@@ -32,6 +32,7 @@ import (
"github.com/apache/skywalking-banyandb/banyand/measure"
"github.com/apache/skywalking-banyandb/pkg/bus"
"github.com/apache/skywalking-banyandb/pkg/logger"
+ pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
"github.com/apache/skywalking-banyandb/pkg/query"
"github.com/apache/skywalking-banyandb/pkg/query/executor"
logical_measure "github.com/apache/skywalking-banyandb/pkg/query/logical/measure"
@@ -181,7 +182,46 @@ func (t *topNQueryProcessor) Rev(ctx context.Context, message bus.Message) (resp
}
}()
- resp = bus.NewMessage(bus.MessageID(now), toTopNResponse(result))
+ if request.GetAgg() != modelv1.AggregationFunction_AGGREGATION_FUNCTION_UNSPECIFIED {
+ // Standalone mode: use TopNPostProcessor for aggregation
+ if len(result) == 0 {
+ resp = bus.NewMessage(bus.MessageID(now), &measurev1.TopNResponse{})
+ return
+ }
+ aggregator := measure.CreateTopNPostProcessor(
+ request.GetTopN(),
+ request.GetAgg(),
+ request.GetFieldValueSort(),
+ )
+ var tags []string
+ for _, dp := range result {
+ dpTags := dp.GetTagFamilies()[0].GetTags()
+ if tags == nil {
+ tags = make([]string, len(dpTags))
+ for i, tag := range dpTags {
+ tags[i] = tag.Key
+ }
+ }
+ entityValues := make(pbv1.EntityValues, 0, len(dpTags))
+ for _, tag := range dpTags {
+ entityValues = append(entityValues, tag.Value)
+ }
+ aggregator.Put(entityValues,
+ dp.GetFields()[0].GetValue().GetInt().GetValue(),
+ uint64(dp.GetTimestamp().AsTime().UnixMilli()),
+ dp.GetVersion())
+ }
+ lists, err := aggregator.Val(tags)
+ if err != nil {
+ ml.Error().Err(err).RawJSON("req", logger.Proto(request)).Msg("fail to aggregate topN results")
+ resp = bus.NewMessage(bus.MessageID(now), common.NewError("fail to aggregate topN results for measure %s: %v", request.Name, err))
+ return
+ }
+ resp = bus.NewMessage(bus.MessageID(now), &measurev1.TopNResponse{Lists: lists})
+ } else {
+ // Standalone mode without aggregation or distributed DataNode mode: return top N distinct entities directly
+ resp = bus.NewMessage(bus.MessageID(now), toTopNResponse(result))
+ }
if !request.Trace && t.slowQuery > 0 {
latency := time.Since(n)
if latency > t.slowQuery {
diff --git a/pkg/query/logical/measure/topn_analyzer.go b/pkg/query/logical/measure/topn_analyzer.go
index cf2d87b68..66c29f9ca 100644
--- a/pkg/query/logical/measure/topn_analyzer.go
+++ b/pkg/query/logical/measure/topn_analyzer.go
@@ -109,23 +109,7 @@ func TopNAnalyze(criteria *measurev1.TopNRequest, sourceMeasureSchemaList []*dat
return nil, err
}
- if criteria.GetAgg() != 0 {
- groupByProjectionTags := sourceMeasureSchema.GetEntity().GetTagNames()
- groupByTags := [][]*logical.Tag{logical.NewTags(measure.TopNTagFamily, groupByProjectionTags...)}
- plan = newUnresolvedGroupBy(plan, groupByTags, false)
- plan = newUnresolvedAggregation(plan,
- &logical.Field{Name: topNAggSchema.FieldName},
- criteria.GetAgg(),
- true,
- false,
- false)
- }
-
- plan = top(plan, &measurev1.QueryRequest_Top{
- Number: criteria.GetTopN(),
- FieldName: topNAggSchema.FieldName,
- FieldValueSort: criteria.GetFieldValueSort(),
- })
+ plan = topNDistinct(plan, criteria.GetTopN(), criteria.GetFieldValueSort(), topNAggSchema.FieldName)
p, err := plan.Analyze(s)
if err != nil {
diff --git a/pkg/query/logical/measure/topn_plan_distinct.go b/pkg/query/logical/measure/topn_plan_distinct.go
new file mode 100644
index 000000000..698252e40
--- /dev/null
+++ b/pkg/query/logical/measure/topn_plan_distinct.go
@@ -0,0 +1,268 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 measure
+
+import (
+ "container/heap"
+ "context"
+ "fmt"
+ "sort"
+
+ "github.com/pkg/errors"
+ "go.uber.org/multierr"
+
+ measurev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/measure/v1"
+ modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
+ pbv1 "github.com/apache/skywalking-banyandb/pkg/pb/v1"
+ "github.com/apache/skywalking-banyandb/pkg/query/executor"
+ "github.com/apache/skywalking-banyandb/pkg/query/logical"
+)
+
+// dedupHeapItem tracks an entity's best value and position in the deduplication heap.
+type dedupHeapItem struct {
+ idp *measurev1.InternalDataPoint
+ key string
+ val int64
+ index int
+}
+
+// entityDedupTopN is a heap-based structure that maintains top N distinct entities.
+// For DESC sorting, it uses a min-heap (root = smallest of top N).
+// For ASC sorting, it uses a max-heap (root = largest of top N).
+// The entityMap only tracks entities currently in the heap, so memory is bounded to O(topN).
+type entityDedupTopN struct {
+ entityMap map[string]*dedupHeapItem
+ items []*dedupHeapItem
+ topN int
+ sortDesc bool
+}
+
+// newEntityDedupTopN creates a new entity deduplication heap.
+func newEntityDedupTopN(topN int, sortDesc bool) *entityDedupTopN {
+ return &entityDedupTopN{
+ entityMap: make(map[string]*dedupHeapItem),
+ items: make([]*dedupHeapItem, 0, topN),
+ topN: topN,
+ sortDesc: sortDesc,
+ }
+}
+
+func (h *entityDedupTopN) Len() int { return len(h.items) }
+
+func (h *entityDedupTopN) Less(i, j int) bool {
+ if h.sortDesc {
+ return h.items[i].val < h.items[j].val
+ }
+ return h.items[i].val > h.items[j].val
+}
+
+func (h *entityDedupTopN) Swap(i, j int) {
+ h.items[i], h.items[j] = h.items[j], h.items[i]
+ h.items[i].index = i
+ h.items[j].index = j
+}
+
+func (h *entityDedupTopN) Push(x any) {
+ item := x.(*dedupHeapItem)
+ item.index = len(h.items)
+ h.entityMap[item.key] = item
+ h.items = append(h.items, item)
+}
+
+func (h *entityDedupTopN) Pop() any {
+ old := h.items
+ n := len(old)
+ item := old[n-1]
+ old[n-1] = nil
+ item.index = -1
+ delete(h.entityMap, item.key)
+ h.items = old[0 : n-1]
+ return item
+}
+
+func (h *entityDedupTopN) isBetter(newVal, existingVal int64) bool {
+ if h.sortDesc {
+ return newVal > existingVal
+ }
+ return newVal < existingVal
+}
+
+// Put inserts or updates an entity in the deduplication heap.
+func (h *entityDedupTopN) Put(key string, val int64, idp *measurev1.InternalDataPoint) {
+ if item, exists := h.entityMap[key]; exists {
+ if !h.isBetter(val, item.val) {
+ return
+ }
+ item.val = val
+ item.idp = idp
+ heap.Fix(h, item.index)
+ return
+ }
+ h.tryAddToHeap(key, val, idp)
+}
+
+func (h *entityDedupTopN) tryAddToHeap(key string, val int64, idp *measurev1.InternalDataPoint) {
+ if h.topN <= 0 {
+ return
+ }
+ if len(h.items) < h.topN {
+ heap.Push(h, &dedupHeapItem{key: key, val: val, idp: idp})
+ return
+ }
+ root := h.items[0]
+ if h.isBetter(val, root.val) {
+ heap.Pop(h)
+ heap.Push(h, &dedupHeapItem{key: key, val: val, idp: idp})
+ }
+}
+
+// Elements returns the top N distinct entities sorted by best value.
+func (h *entityDedupTopN) Elements() []*measurev1.InternalDataPoint {
+ result := make([]*dedupHeapItem, len(h.items))
+ copy(result, h.items)
+ sort.Slice(result, func(i, j int) bool {
+ if h.sortDesc {
+ return result[i].val > result[j].val
+ }
+ return result[i].val < result[j].val
+ })
+ idps := make([]*measurev1.InternalDataPoint, len(result))
+ for i, item := range result {
+ idps[i] = item.idp
+ }
+ return idps
+}
+
+var _ logical.UnresolvedPlan = (*unresolvedTopNDistinct)(nil)
+
+// unresolvedTopNDistinct is an unresolved plan for top N distinct entity selection.
+type unresolvedTopNDistinct struct {
+ unresolvedInput logical.UnresolvedPlan
+ fieldName string
+ topN int32
+ sort modelv1.Sort
+}
+
+func topNDistinct(input logical.UnresolvedPlan, topN int32, sort modelv1.Sort, fieldName string) logical.UnresolvedPlan {
+ return &unresolvedTopNDistinct{
+ unresolvedInput: input,
+ topN: topN,
+ sort: sort,
+ fieldName: fieldName,
+ }
+}
+
+func (u *unresolvedTopNDistinct) Analyze(measureSchema logical.Schema) (logical.Plan, error) {
+ prevPlan, analyzeErr := u.unresolvedInput.Analyze(measureSchema)
+ if analyzeErr != nil {
+ return nil, analyzeErr
+ }
+ fieldRefs, fieldErr := prevPlan.Schema().CreateFieldRef(logical.NewField(u.fieldName))
+ if fieldErr != nil {
+ return nil, fieldErr
+ }
+ if len(fieldRefs) == 0 {
+ return nil, errors.New("no field ref found for topN distinct")
+ }
+ sortDesc := u.sort == modelv1.Sort_SORT_DESC
+ return &topNDistinctOp{
+ Parent: &logical.Parent{
+ UnresolvedInput: u.unresolvedInput,
+ Input: prevPlan,
+ },
+ topN: int(u.topN),
+ sortDesc: sortDesc,
+ fieldIdx: fieldRefs[0].Spec.FieldIdx,
+ }, nil
+}
+
+var _ logical.Plan = (*topNDistinctOp)(nil)
+
+// topNDistinctOp selects top N distinct entities by best value.
+type topNDistinctOp struct {
+ *logical.Parent
+ topN int
+ sortDesc bool
+ fieldIdx int
+}
+
+func (t *topNDistinctOp) String() string {
+ dir := "DESC"
+ if !t.sortDesc {
+ dir = "ASC"
+ }
+ return fmt.Sprintf("TopNDistinct(topN=%d,sort=%s)", t.topN, dir)
+}
+
+func (t *topNDistinctOp) Children() []logical.Plan {
+ return []logical.Plan{t.Input}
+}
+
+func (t *topNDistinctOp) Schema() logical.Schema {
+ return t.Input.Schema()
+}
+
+func (t *topNDistinctOp) Execute(ctx context.Context) (mit executor.MIterator, err error) {
+ iter, execErr := t.Input.(executor.MeasureExecutable).Execute(ctx)
+ if execErr != nil {
+ return nil, execErr
+ }
+ defer func() {
+ err = multierr.Append(err, iter.Close())
+ }()
+ edHeap := newEntityDedupTopN(t.topN, t.sortDesc)
+ for iter.Next() {
+ dataPoints := iter.Current()
+ for _, idp := range dataPoints {
+ dp := idp.GetDataPoint()
+ if len(dp.GetTagFamilies()) == 0 || len(dp.GetTagFamilies()[0].GetTags()) == 0 {
+ continue
+ }
+ entityValues := make(pbv1.EntityValues, 0, len(dp.GetTagFamilies()[0].GetTags()))
+ for _, tag := range dp.GetTagFamilies()[0].GetTags() {
+ entityValues = append(entityValues, tag.Value)
+ }
+ entityKey := entityValues.String()
+ fieldVal := dp.GetFields()[t.fieldIdx].GetValue().GetInt().GetValue()
+ edHeap.Put(entityKey, fieldVal, idp)
+ }
+ }
+ return &topNDistinctIterator{
+ elements: edHeap.Elements(),
+ index: -1,
+ }, nil
+}
+
+// topNDistinctIterator iterates over the top N distinct entity results.
+type topNDistinctIterator struct {
+ elements []*measurev1.InternalDataPoint
+ index int
+}
+
+func (t *topNDistinctIterator) Next() bool {
+ t.index++
+ return t.index < len(t.elements)
+}
+
+func (t *topNDistinctIterator) Current() []*measurev1.InternalDataPoint {
+ return []*measurev1.InternalDataPoint{t.elements[t.index]}
+}
+
+func (t *topNDistinctIterator) Close() error {
+ return nil
+}
diff --git a/pkg/query/logical/measure/topn_plan_distinct_test.go b/pkg/query/logical/measure/topn_plan_distinct_test.go
new file mode 100644
index 000000000..b9d1608c3
--- /dev/null
+++ b/pkg/query/logical/measure/topn_plan_distinct_test.go
@@ -0,0 +1,173 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 measure
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ measurev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/measure/v1"
+ modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
+)
+
+func strTagValue(v string) *modelv1.TagValue {
+ return &modelv1.TagValue{Value: &modelv1.TagValue_Str{Str: &modelv1.Str{Value: v}}}
+}
+
+func intFieldValue(v int64) *modelv1.FieldValue {
+ return &modelv1.FieldValue{Value: &modelv1.FieldValue_Int{Int: &modelv1.Int{Value: v}}}
+}
+
+func makeIDP(entityValues []*modelv1.TagValue, fieldVal int64) *measurev1.InternalDataPoint {
+ tags := make([]*modelv1.Tag, len(entityValues))
+ for i, ev := range entityValues {
+ tags[i] = &modelv1.Tag{Key: "entity", Value: ev}
+ }
+ return &measurev1.InternalDataPoint{
+ DataPoint: &measurev1.DataPoint{
+ TagFamilies: []*modelv1.TagFamily{
+ {Name: "_topN", Tags: tags},
+ },
+ Fields: []*measurev1.DataPoint_Field{
+ {
+ Name: "value",
+ Value: intFieldValue(fieldVal),
+ },
+ },
+ },
+ }
+}
+
+func TestEntityDedupTopN_DESC(t *testing.T) {
+ h := newEntityDedupTopN(3, true)
+
+ h.Put("A", 100, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 100))
+ h.Put("B", 90, makeIDP([]*modelv1.TagValue{strTagValue("B")}, 90))
+ h.Put("A", 200, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 200))
+ h.Put("C", 80, makeIDP([]*modelv1.TagValue{strTagValue("C")}, 80))
+ h.Put("D", 150, makeIDP([]*modelv1.TagValue{strTagValue("D")}, 150))
+
+ result := h.Elements()
+ require.Len(t, result, 3)
+ vals := extractFieldValues(result)
+ assert.Equal(t, []int64{200, 150, 90}, vals)
+}
+
+func TestEntityDedupTopN_ASC(t *testing.T) {
+ h := newEntityDedupTopN(3, false)
+
+ h.Put("A", 100, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 100))
+ h.Put("B", 90, makeIDP([]*modelv1.TagValue{strTagValue("B")}, 90))
+ h.Put("A", 50, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 50))
+ h.Put("C", 80, makeIDP([]*modelv1.TagValue{strTagValue("C")}, 80))
+ h.Put("D", 30, makeIDP([]*modelv1.TagValue{strTagValue("D")}, 30))
+
+ result := h.Elements()
+ require.Len(t, result, 3)
+ vals := extractFieldValues(result)
+ assert.Equal(t, []int64{30, 50, 80}, vals)
+}
+
+func TestEntityDedupTopN_EvictAndReAdd(t *testing.T) {
+ h := newEntityDedupTopN(2, true)
+
+ h.Put("A", 100, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 100))
+ h.Put("B", 90, makeIDP([]*modelv1.TagValue{strTagValue("B")}, 90))
+ h.Put("C", 80, makeIDP([]*modelv1.TagValue{strTagValue("C")}, 80))
+ h.Put("C", 95, makeIDP([]*modelv1.TagValue{strTagValue("C")}, 95))
+
+ result := h.Elements()
+ require.Len(t, result, 2)
+ vals := extractFieldValues(result)
+ assert.Equal(t, []int64{100, 95}, vals)
+}
+
+func TestEntityDedupTopN_EmptyInput(t *testing.T) {
+ h := newEntityDedupTopN(3, true)
+ result := h.Elements()
+ assert.Empty(t, result)
+}
+
+func TestEntityDedupTopN_TopN1(t *testing.T) {
+ h := newEntityDedupTopN(1, true)
+ h.Put("A", 100, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 100))
+ h.Put("B", 200, makeIDP([]*modelv1.TagValue{strTagValue("B")}, 200))
+ h.Put("A", 50, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 50))
+ h.Put("B", 300, makeIDP([]*modelv1.TagValue{strTagValue("B")}, 300))
+
+ result := h.Elements()
+ require.Len(t, result, 1)
+ vals := extractFieldValues(result)
+ assert.Equal(t, []int64{300}, vals)
+}
+
+func TestEntityDedupTopN_AllSameEntity(t *testing.T) {
+ h := newEntityDedupTopN(3, true)
+ h.Put("A", 100, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 100))
+ h.Put("A", 200, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 200))
+ h.Put("A", 150, makeIDP([]*modelv1.TagValue{strTagValue("A")}, 150))
+
+ result := h.Elements()
+ require.Len(t, result, 1)
+ vals := extractFieldValues(result)
+ assert.Equal(t, []int64{200}, vals)
+}
+
+func extractFieldValues(idps []*measurev1.InternalDataPoint) []int64 {
+ vals := make([]int64, len(idps))
+ for i, idp := range idps {
+ vals[i] = idp.GetDataPoint().GetFields()[0].GetValue().GetInt().GetValue()
+ }
+ return vals
+}
+
+func TestTopNDistinctIterator(t *testing.T) {
+ items := []*measurev1.InternalDataPoint{
+ makeIDP([]*modelv1.TagValue{strTagValue("svc-A")}, 200),
+ makeIDP([]*modelv1.TagValue{strTagValue("svc-D")}, 150),
+ makeIDP([]*modelv1.TagValue{strTagValue("svc-B")}, 90),
+ }
+ iter := &topNDistinctIterator{elements: items, index: -1}
+
+ var result []*measurev1.InternalDataPoint
+ for iter.Next() {
+ current := iter.Current()
+ result = append(result, current[0])
+ }
+ require.NoError(t, iter.Close())
+
+ require.Len(t, result, 3)
+ vals := extractFieldValues(result)
+ assert.Equal(t, []int64{200, 150, 90}, vals)
+}
+
+func TestTopNDistinctIterator_Empty(t *testing.T) {
+ iter := &topNDistinctIterator{elements: nil, index: -1}
+ require.False(t, iter.Next())
+ require.NoError(t, iter.Close())
+}
+
+func TestTopNDistinctOp_String(t *testing.T) {
+ op := &topNDistinctOp{topN: 10, sortDesc: true}
+ assert.Equal(t, "TopNDistinct(topN=10,sort=DESC)", op.String())
+
+ opAsc := &topNDistinctOp{topN: 5, sortDesc: false}
+ assert.Equal(t, "TopNDistinct(topN=5,sort=ASC)", opAsc.String())
+}
diff --git a/pkg/test/measure/testdata/measures/service_instance_metric_topn_test.json b/pkg/test/measure/testdata/measures/service_instance_metric_topn_test.json
new file mode 100644
index 000000000..82b97bddc
--- /dev/null
+++ b/pkg/test/measure/testdata/measures/service_instance_metric_topn_test.json
@@ -0,0 +1,41 @@
+{
+ "metadata": {
+ "name": "service_instance_metric_topn_test",
+ "group": "sw_metric"
+ },
+ "tag_families": [
+ {
+ "name": "default",
+ "tags": [
+ {
+ "name": "id",
+ "type": "TAG_TYPE_STRING"
+ },
+ {
+ "name": "service_id",
+ "type": "TAG_TYPE_STRING"
+ }
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "name": "value",
+ "field_type": "FIELD_TYPE_INT",
+ "encoding_method": "ENCODING_METHOD_GORILLA",
+ "compression_method": "COMPRESSION_METHOD_ZSTD"
+ }
+ ],
+ "entity": {
+ "tag_names": [
+ "service_id"
+ ]
+ },
+ "sharding_key": {
+ "tag_names": [
+ "service_id"
+ ]
+ },
+ "interval": "1m",
+ "updated_at": "2021-04-15T01:30:15.01Z"
+}
diff --git a/pkg/test/measure/testdata/topn_aggregations/topn_agg_test.json b/pkg/test/measure/testdata/topn_aggregations/topn_agg_test.json
new file mode 100644
index 000000000..9403cfe7d
--- /dev/null
+++ b/pkg/test/measure/testdata/topn_aggregations/topn_agg_test.json
@@ -0,0 +1,17 @@
+{
+ "metadata": {
+ "name": "topn_agg_test",
+ "group": "sw_metric"
+ },
+ "source_measure": {
+ "name": "service_instance_metric_topn_test",
+ "group": "sw_metric"
+ },
+ "field_name": "value",
+ "field_value_sort": 0,
+ "group_by_tag_names": [
+ "service_id"
+ ],
+ "counters_number": 1000,
+ "lru_size": 10
+}
diff --git a/test/cases/init.go b/test/cases/init.go
index f5b167200..401ecc27e 100644
--- a/test/cases/init.go
+++ b/test/cases/init.go
@@ -101,6 +101,7 @@ func Initialize(addr string, now time.Time) {
casesmeasuredata.Write(conn, "service_cpm_minute", "sw_updated", "service_cpm_minute_updated_data.json", now.Add(10*time.Minute), interval)
casesmeasuredata.Write(conn, "endpoint_resp_time_minute", "sw_metric", "endpoint_resp_time_minute_data.json", now, interval)
casesmeasuredata.Write(conn, "endpoint_resp_time_minute", "sw_metric", "endpoint_resp_time_minute_data1.json", now.Add(10*time.Second), interval)
+ casesmeasuredata.Write(conn, "service_instance_metric_topn_test", "sw_metric", "service_instance_metric_topn_test_data.json", now, interval)
casesmeasuredata.WriteMixed(conn, now.Add(30*time.Minute), interval,
casesmeasuredata.WriteSpec{
Metadata: &commonv1.Metadata{Name: "service_cpm_minute", Group: "sw_spec"},
diff --git a/test/cases/measure/data/testdata/service_instance_metric_topn_test_data.json b/test/cases/measure/data/testdata/service_instance_metric_topn_test_data.json
new file mode 100644
index 000000000..58b68eda9
--- /dev/null
+++ b/test/cases/measure/data/testdata/service_instance_metric_topn_test_data.json
@@ -0,0 +1,152 @@
+[
+ {
+ "tag_families": [
+ {
+ "tags": [
+ {
+ "str": {
+ "value": "1"
+ }
+ },
+ {
+ "str": {
+ "value": "svc_1"
+ }
+ }
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "int": {
+ "value": 100
+ }
+ }
+ ]
+ },
+ {
+ "tag_families": [
+ {
+ "tags": [
+ {
+ "str": {
+ "value": "2"
+ }
+ },
+ {
+ "str": {
+ "value": "svc_1"
+ }
+ }
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "int": {
+ "value": 200
+ }
+ }
+ ]
+ },
+ {
+ "tag_families": [
+ {
+ "tags": [
+ {
+ "str": {
+ "value": "3"
+ }
+ },
+ {
+ "str": {
+ "value": "svc_1"
+ }
+ }
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "int": {
+ "value": 151
+ }
+ }
+ ]
+ },
+ {
+ "tag_families": [
+ {
+ "tags": [
+ {
+ "str": {
+ "value": "4"
+ }
+ },
+ {
+ "str": {
+ "value": "svc_2"
+ }
+ }
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "int": {
+ "value": 300
+ }
+ }
+ ]
+ },
+ {
+ "tag_families": [
+ {
+ "tags": [
+ {
+ "str": {
+ "value": "5"
+ }
+ },
+ {
+ "str": {
+ "value": "svc_2"
+ }
+ }
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "int": {
+ "value": 50
+ }
+ }
+ ]
+ },
+ {
+ "tag_families": [
+ {
+ "tags": [
+ {
+ "str": {
+ "value": "6"
+ }
+ },
+ {
+ "str": {
+ "value": "svc_3"
+ }
+ }
+ ]
+ }
+ ],
+ "fields": [
+ {
+ "int": {
+ "value": 180
+ }
+ }
+ ]
+ }
+]
diff --git a/test/cases/topn/data/data.go b/test/cases/topn/data/data.go
index b00e41868..1ffa4fff0 100644
--- a/test/cases/topn/data/data.go
+++ b/test/cases/topn/data/data.go
@@ -22,6 +22,7 @@ import (
"context"
"embed"
"regexp"
+ "slices"
"strings"
"github.com/google/go-cmp/cmp"
@@ -37,6 +38,7 @@ import (
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
measurev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/measure/v1"
+ modelv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/model/v1"
"github.com/apache/skywalking-banyandb/banyand/metadata"
"github.com/apache/skywalking-banyandb/banyand/metadata/schema"
"github.com/apache/skywalking-banyandb/pkg/bydbql"
@@ -82,6 +84,13 @@ var VerifyFn = func(innerGm gm.Gomega, sharedContext helpers.SharedContext, args
innerGm.Expect(err).NotTo(gm.HaveOccurred())
want := &measurev1.TopNResponse{}
helpers.UnmarshalYAML(ww, want)
+ // Sort items by value, using entity as tie-breaker for deterministic ordering.
+ for _, list := range resp.Lists {
+ slices.SortFunc(list.Items, compareTopNItems)
+ }
+ for _, list := range want.Lists {
+ slices.SortFunc(list.Items, compareTopNItems)
+ }
success := innerGm.Expect(cmp.Equal(resp, want,
protocmp.IgnoreUnknown(),
protocmp.IgnoreFields(&measurev1.TopNList{}, "timestamp"),
@@ -109,6 +118,88 @@ var VerifyFn = func(innerGm gm.Gomega, sharedContext helpers.SharedContext, args
innerGm.Expect(resp.Trace.GetSpans()).NotTo(gm.BeEmpty())
}
+// compareTopNItems compares two TopNList_Item by value first, then by entity tags for deterministic ordering.
+func compareTopNItems(a, b *measurev1.TopNList_Item) int {
+ // Primary: sort by value to preserve ranking verification.
+ aVal := a.GetValue().GetInt().GetValue()
+ bVal := b.GetValue().GetInt().GetValue()
+ if aVal < bVal {
+ return -1
+ }
+ if aVal > bVal {
+ return 1
+ }
+ // Secondary: sort by entity tags as tie-breaker for equal values.
+ for tagIdx, aTag := range a.Entity {
+ if tagIdx >= len(b.Entity) {
+ return 1
+ }
+ if tagCmp := compareTagValue(aTag.GetValue(), b.Entity[tagIdx].GetValue()); tagCmp != 0 {
+ return tagCmp
+ }
+ }
+ if len(a.Entity) < len(b.Entity) {
+ return -1
+ }
+ return 0
+}
+
+// compareTagValue compares two TagValue instances in a type-aware manner.
+func compareTagValue(a, b *modelv1.TagValue) int {
+ aOrd := tagValOrder(a)
+ bOrd := tagValOrder(b)
+ if aOrd != bOrd {
+ return aOrd - bOrd
+ }
+ switch a.GetValue().(type) {
+ case *modelv1.TagValue_Str:
+ aVal := a.GetStr().GetValue()
+ bVal := b.GetStr().GetValue()
+ switch {
+ case aVal < bVal:
+ return -1
+ case aVal > bVal:
+ return 1
+ default:
+ return 0
+ }
+ case *modelv1.TagValue_Int:
+ aVal := a.GetInt().GetValue()
+ bVal := b.GetInt().GetValue()
+ switch {
+ case aVal < bVal:
+ return -1
+ case aVal > bVal:
+ return 1
+ default:
+ return 0
+ }
+ default:
+ return 0
+ }
+}
+
+func tagValOrder(v *modelv1.TagValue) int {
+ switch v.GetValue().(type) {
+ case *modelv1.TagValue_Null:
+ return 0
+ case *modelv1.TagValue_Str:
+ return 1
+ case *modelv1.TagValue_Int:
+ return 2
+ case *modelv1.TagValue_StrArray:
+ return 3
+ case *modelv1.TagValue_IntArray:
+ return 4
+ case *modelv1.TagValue_BinaryData:
+ return 5
+ case *modelv1.TagValue_Timestamp:
+ return 6
+ default:
+ return -1
+ }
+}
+
// verifyQLWithRequest verifies that the QL file produces an equivalent QueryRequest to the YAML.
func verifyQLWithRequest(ctx context.Context, innerGm gm.Gomega, args helpers.Args, yamlQuery *measurev1.TopNRequest, conn *grpclib.ClientConn) {
qlContent, err := qlFS.ReadFile("input/" + args.Input + ".ql")
diff --git a/test/cases/topn/data/input/topn_count.ql b/test/cases/topn/data/input/topn_count.ql
new file mode 100644
index 000000000..59bb42fa4
--- /dev/null
+++ b/test/cases/topn/data/input/topn_count.ql
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+SHOW TOP 3
+FROM MEASURE topn_agg_test IN sw_metric
+TIME > '-15m'
+AGGREGATE BY COUNT
+ORDER BY DESC
diff --git a/test/cases/topn/data/input/topn_count.yaml b/test/cases/topn/data/input/topn_count.yaml
new file mode 100644
index 000000000..e4c366780
--- /dev/null
+++ b/test/cases/topn/data/input/topn_count.yaml
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+name: "topn_agg_test"
+groups: ["sw_metric"]
+topN: 3
+fieldValueSort: 1
+agg: 4
diff --git a/test/cases/topn/data/input/topn_max.ql b/test/cases/topn/data/input/topn_max.ql
new file mode 100644
index 000000000..18f0c542f
--- /dev/null
+++ b/test/cases/topn/data/input/topn_max.ql
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+SHOW TOP 3
+FROM MEASURE topn_agg_test IN sw_metric
+TIME > '-15m'
+AGGREGATE BY MAX
+ORDER BY DESC
diff --git a/test/cases/topn/data/input/topn_max.yaml b/test/cases/topn/data/input/topn_max.yaml
new file mode 100644
index 000000000..1ed40d2f2
--- /dev/null
+++ b/test/cases/topn/data/input/topn_max.yaml
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+name: "topn_agg_test"
+groups: ["sw_metric"]
+topN: 3
+fieldValueSort: 1
+agg: 2
diff --git a/test/cases/topn/data/input/topn_mean.ql b/test/cases/topn/data/input/topn_mean.ql
new file mode 100644
index 000000000..aa6d8f691
--- /dev/null
+++ b/test/cases/topn/data/input/topn_mean.ql
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+SHOW TOP 2
+FROM MEASURE topn_agg_test IN sw_metric
+TIME > '-15m'
+AGGREGATE BY MEAN
+ORDER BY DESC
diff --git a/test/cases/topn/data/input/topn_mean.yaml b/test/cases/topn/data/input/topn_mean.yaml
new file mode 100644
index 000000000..0c8ab3513
--- /dev/null
+++ b/test/cases/topn/data/input/topn_mean.yaml
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+name: "topn_agg_test"
+groups: ["sw_metric"]
+topN: 2
+fieldValueSort: 1
+agg: 1
diff --git a/test/cases/topn/data/input/topn_min.ql b/test/cases/topn/data/input/topn_min.ql
new file mode 100644
index 000000000..d1e756f42
--- /dev/null
+++ b/test/cases/topn/data/input/topn_min.ql
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+SHOW TOP 3
+FROM MEASURE topn_agg_test IN sw_metric
+TIME > '-15m'
+AGGREGATE BY MIN
+ORDER BY ASC
diff --git a/test/cases/topn/data/input/topn_min.yaml b/test/cases/topn/data/input/topn_min.yaml
new file mode 100644
index 000000000..1c9263e9b
--- /dev/null
+++ b/test/cases/topn/data/input/topn_min.yaml
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+name: "topn_agg_test"
+groups: ["sw_metric"]
+topN: 3
+fieldValueSort: 2
+agg: 3
diff --git a/test/cases/topn/data/input/topn_sum.ql b/test/cases/topn/data/input/topn_sum.ql
new file mode 100644
index 000000000..5d6f80363
--- /dev/null
+++ b/test/cases/topn/data/input/topn_sum.ql
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+SHOW TOP 3
+FROM MEASURE topn_agg_test IN sw_metric
+TIME > '-15m'
+AGGREGATE BY SUM
+ORDER BY DESC
diff --git a/test/cases/topn/data/input/topn_sum.yaml b/test/cases/topn/data/input/topn_sum.yaml
new file mode 100644
index 000000000..4bf8dc541
--- /dev/null
+++ b/test/cases/topn/data/input/topn_sum.yaml
@@ -0,0 +1,22 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+name: "topn_agg_test"
+groups: ["sw_metric"]
+topN: 3
+fieldValueSort: 1
+agg: 5
diff --git a/test/cases/topn/data/want/topn_count.yaml b/test/cases/topn/data/want/topn_count.yaml
new file mode 100644
index 000000000..bca240308
--- /dev/null
+++ b/test/cases/topn/data/want/topn_count.yaml
@@ -0,0 +1,43 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+lists:
+- items:
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_2
+ value:
+ int:
+ value: "1"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_1
+ value:
+ int:
+ value: "1"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_3
+ value:
+ int:
+ value: "1"
diff --git a/test/cases/topn/data/want/topn_max.yaml b/test/cases/topn/data/want/topn_max.yaml
new file mode 100644
index 000000000..6a48d11af
--- /dev/null
+++ b/test/cases/topn/data/want/topn_max.yaml
@@ -0,0 +1,43 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+lists:
+- items:
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_2
+ value:
+ int:
+ value: "300"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_1
+ value:
+ int:
+ value: "200"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_3
+ value:
+ int:
+ value: "180"
diff --git a/test/cases/topn/data/want/topn_mean.yaml b/test/cases/topn/data/want/topn_mean.yaml
new file mode 100644
index 000000000..7016d75dc
--- /dev/null
+++ b/test/cases/topn/data/want/topn_mean.yaml
@@ -0,0 +1,35 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+lists:
+- items:
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_2
+ value:
+ int:
+ value: "300"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_1
+ value:
+ int:
+ value: "200"
diff --git a/test/cases/topn/data/want/topn_min.yaml b/test/cases/topn/data/want/topn_min.yaml
new file mode 100644
index 000000000..d77ced46d
--- /dev/null
+++ b/test/cases/topn/data/want/topn_min.yaml
@@ -0,0 +1,43 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+lists:
+- items:
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_2
+ value:
+ int:
+ value: "50"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_1
+ value:
+ int:
+ value: "100"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_3
+ value:
+ int:
+ value: "180"
diff --git a/test/cases/topn/data/want/topn_sum.yaml b/test/cases/topn/data/want/topn_sum.yaml
new file mode 100644
index 000000000..6a48d11af
--- /dev/null
+++ b/test/cases/topn/data/want/topn_sum.yaml
@@ -0,0 +1,43 @@
+# Licensed to Apache Software Foundation (ASF) under one or more contributor
+# license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright
+# ownership. Apache Software Foundation (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.
+
+lists:
+- items:
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_2
+ value:
+ int:
+ value: "300"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_1
+ value:
+ int:
+ value: "200"
+ - entity:
+ - key: service_id
+ value:
+ str:
+ value: svc_3
+ value:
+ int:
+ value: "180"
diff --git a/test/cases/topn/topn.go b/test/cases/topn/topn.go
index 2664b2391..d08317f13 100644
--- a/test/cases/topn/topn.go
+++ b/test/cases/topn/topn.go
@@ -42,7 +42,7 @@ var (
}
)
-var _ = g.DescribeTable("TopN Tests", verify,
+var topnEntries = []any{
g.Entry("max top3 order by desc", helpers.Args{Input: "aggr_desc", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("max top3 with condition order by desc", helpers.Args{Input: "condition_aggr_desc", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("max top3 for null group order by desc", helpers.Args{Input: "null_group", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
@@ -52,4 +52,16 @@ var _ = g.DescribeTable("TopN Tests", verify,
g.Entry("using in operation in aggregation", helpers.Args{Input: "in", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("using not-in operation in aggregation", helpers.Args{Input: "not_in", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
g.Entry("max top3 with version merged order by desc", helpers.Args{Input: "aggr_version_merged", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
-)
+ g.Entry("int64 max aggregation test", helpers.Args{Input: "topn_max", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("int64 min aggregation test", helpers.Args{Input: "topn_min", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("int64 sum aggregation test", helpers.Args{Input: "topn_sum", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("int64 mean aggregation test", helpers.Args{Input: "topn_mean", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+ g.Entry("int64 count aggregation test", helpers.Args{Input: "topn_count", Duration: 25 * time.Minute, Offset: -20 * time.Minute}),
+}
+
+// RegisterTable registers the topn test table with the given description.
+func RegisterTable(description string) bool {
+ return g.DescribeTable(description, append([]any{verify}, topnEntries...)...)
+}
+
+var _ = RegisterTable("TopN Tests")
(skywalking-banyandb) 13/24: Fix stale sync request blocking watch session channel (#1080)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit ca6a0d3522d035634ee7f18961701452ce26ffb2
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Tue Apr 21 14:07:52 2026 +0800
Fix stale sync request blocking watch session channel (#1080)
---
banyand/metadata/schema/property/client.go | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/banyand/metadata/schema/property/client.go b/banyand/metadata/schema/property/client.go
index 0d7f8d381..1a52c4130 100644
--- a/banyand/metadata/schema/property/client.go
+++ b/banyand/metadata/schema/property/client.go
@@ -1300,6 +1300,12 @@ func (r *SchemaRegistry) sendSyncRequest(ctx context.Context,
sentChs := make(map[string]chan []*digestEntry, len(sessions))
var skipped int
for name, s := range sessions {
+ // Drain any stale message left from a previous timed-out sync.
+ select {
+ case <-s.syncReqCh:
+ r.l.Debug().Str("node", name).Msg("sendSyncRequest: drained stale sync request")
+ default:
+ }
msg := &syncMessage{
syncRequest: *req,
responseCh: make(chan []*digestEntry, 1),
(skywalking-banyandb) 15/24: Fix FileSystemError not matching io/fs.ErrNotExist sentinel (#1088)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit e11effa4d66139464ab28cc8f8408da75b92478e
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Thu Apr 23 09:09:44 2026 +0800
Fix FileSystemError not matching io/fs.ErrNotExist sentinel (#1088)
---
banyand/internal/storage/tsdb_test.go | 59 ++++++++++++++++++++++++++++++++
pkg/fs/error.go | 22 +++++++++++-
pkg/fs/error_test.go | 63 +++++++++++++++++++++++++++++++++++
3 files changed, 143 insertions(+), 1 deletion(-)
diff --git a/banyand/internal/storage/tsdb_test.go b/banyand/internal/storage/tsdb_test.go
index cf87cb3f5..f07263741 100644
--- a/banyand/internal/storage/tsdb_test.go
+++ b/banyand/internal/storage/tsdb_test.go
@@ -399,6 +399,65 @@ func TestEpochSegmentCleanupOnOpen(t *testing.T) {
require.NoDirExists(t, epochSegDir, "epoch segment should be removed on open")
}
+// TestHalfBornSegmentCleanupOnOpen reproduces the production failure where a
+// segment directory exists on disk without a metadata file (e.g. crash between
+// MkdirPanicIfExist and metadata write, or partial sync receive). Prior to the
+// errors.Is fix in pkg/fs, localFileSystem.Read returned a *FileSystemError
+// that did not satisfy errors.Is(err, io/fs.ErrNotExist), so the segment
+// controller's cleanup branch was never taken and OpenTSDB failed, leaving the
+// group with db=nil forever.
+func TestHalfBornSegmentCleanupOnOpen(t *testing.T) {
+ logger.Init(logger.Logging{
+ Env: "dev",
+ Level: flags.LogLevel,
+ })
+ dir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ opts := TSDBOpts[*MockTSTable, any]{
+ Location: dir,
+ SegmentInterval: IntervalRule{Unit: DAY, Num: 1},
+ TTL: IntervalRule{Unit: DAY, Num: 7},
+ ShardNum: 1,
+ TSTableCreator: MockTSTableCreator,
+ }
+
+ ctx := context.Background()
+ mc := timestamp.NewMockClock()
+ ts, err := time.ParseInLocation("2006-01-02 15:04:05", "2024-05-01 00:00:00", time.Local)
+ require.NoError(t, err)
+ mc.Set(ts)
+ ctx = timestamp.SetClock(ctx, mc)
+
+ sc := NewServiceCache()
+ tsdb1, err := OpenTSDB(ctx, opts, sc, group)
+ require.NoError(t, err)
+ seg, segErr := tsdb1.CreateSegmentIfNotExist(ts)
+ require.NoError(t, segErr)
+ seg.DecRef()
+ require.NoError(t, tsdb1.Close())
+
+ halfBornDir := filepath.Join(dir, "seg-20240502")
+ lfs := fs.NewLocalFileSystem()
+ lfs.MkdirIfNotExist(halfBornDir, DirPerm)
+ require.NoFileExists(t, filepath.Join(halfBornDir, metadataFilename),
+ "half-born segment must have no metadata file to reproduce the bug")
+
+ tsdb2, err := OpenTSDB(ctx, opts, sc, group)
+ require.NoError(t, err, "OpenTSDB must not fail on a half-born segment")
+ require.NotNil(t, tsdb2)
+ defer tsdb2.Close()
+
+ require.NoDirExists(t, halfBornDir, "half-born segment should be removed on open")
+
+ db := tsdb2.(*database[*MockTSTable, any])
+ segs, _ := db.segmentController.segments(false)
+ require.Equal(t, 1, len(segs), "only the valid segment should remain after cleanup")
+ for i := range segs {
+ segs[i].DecRef()
+ }
+}
+
func TestTSDBCollect(t *testing.T) {
logger.Init(logger.Logging{
Env: "dev",
diff --git a/pkg/fs/error.go b/pkg/fs/error.go
index 8247d9e36..55ae695e9 100644
--- a/pkg/fs/error.go
+++ b/pkg/fs/error.go
@@ -18,7 +18,10 @@
// Package fs (file system) is an independent component to operate file and directory.
package fs
-import "fmt"
+import (
+ "fmt"
+ iofs "io/fs"
+)
// FileSystemError code.
const (
@@ -44,3 +47,20 @@ type FileSystemError struct {
func (err *FileSystemError) Error() string {
return fmt.Sprintf("File system return error: %s, error code: %d", err.Message, err.Code)
}
+
+// Is reports whether the error matches one of the standard io/fs sentinel
+// errors, so callers can use errors.Is with iofs.ErrNotExist, iofs.ErrExist
+// and iofs.ErrPermission regardless of the underlying OS error wrapping.
+// The target is compared directly (no recursive errors.Is) to mirror the
+// convention used by syscall.Errno.Is in the standard library.
+func (err *FileSystemError) Is(target error) bool {
+ switch target {
+ case iofs.ErrNotExist:
+ return err.Code == IsNotExistError
+ case iofs.ErrExist:
+ return err.Code == isExistError
+ case iofs.ErrPermission:
+ return err.Code == permissionError
+ }
+ return false
+}
diff --git a/pkg/fs/error_test.go b/pkg/fs/error_test.go
new file mode 100644
index 000000000..797c84077
--- /dev/null
+++ b/pkg/fs/error_test.go
@@ -0,0 +1,63 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 fs
+
+import (
+ "errors"
+ "fmt"
+ iofs "io/fs"
+ "path/filepath"
+ "testing"
+)
+
+func TestFileSystemErrorIsNotExist(t *testing.T) {
+ lfs := NewLocalFileSystem()
+ missing := filepath.Join(t.TempDir(), "does-not-exist")
+ _, readErr := lfs.Read(missing)
+ if readErr == nil {
+ t.Fatalf("expected error reading missing file, got nil")
+ }
+ if !errors.Is(readErr, iofs.ErrNotExist) {
+ t.Fatalf("expected errors.Is(err, iofs.ErrNotExist) to be true for %T: %v", readErr, readErr)
+ }
+}
+
+func TestFileSystemErrorIsMatrix(t *testing.T) {
+ tests := []struct {
+ target error
+ name string
+ code int
+ want bool
+ }{
+ {name: "not-exist-matches", code: IsNotExistError, target: iofs.ErrNotExist, want: true},
+ {name: "exist-matches", code: isExistError, target: iofs.ErrExist, want: true},
+ {name: "permission-matches", code: permissionError, target: iofs.ErrPermission, want: true},
+ {name: "not-exist-vs-exist", code: IsNotExistError, target: iofs.ErrExist, want: false},
+ {name: "read-vs-not-exist", code: readError, target: iofs.ErrNotExist, want: false},
+ {name: "other-vs-permission", code: otherError, target: iofs.ErrPermission, want: false},
+ {name: "wrapped-not-exist-does-not-match", code: IsNotExistError, target: fmt.Errorf("wrapped: %w", iofs.ErrNotExist), want: false},
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ err := &FileSystemError{Code: tc.code, Message: "test"}
+ if got := errors.Is(err, tc.target); got != tc.want {
+ t.Fatalf("errors.Is(code=%d, %v) = %v, want %v", tc.code, tc.target, got, tc.want)
+ }
+ })
+ }
+}
(skywalking-banyandb) 08/24: fix(handoff): prevent size limit bypass and sidx timestamp corruption in handoff replay (#1064)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit a8a083e4451afe898073dbc6f3198ff9762c989d
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Sun Apr 12 12:51:04 2026 +0800
fix(handoff): prevent size limit bypass and sidx timestamp corruption in handoff replay (#1064)
---
banyand/measure/tstable.go | 8 +
banyand/stream/tstable.go | 8 +
banyand/trace/handoff_controller.go | 113 ++++++---
banyand/trace/handoff_replay_test.go | 332 +++++++++++++++++++++++++
banyand/trace/handoff_storage_test.go | 56 +++++
banyand/trace/tstable.go | 8 +
test/integration/handoff/handoff_suite_test.go | 44 ++++
7 files changed, 538 insertions(+), 31 deletions(-)
diff --git a/banyand/measure/tstable.go b/banyand/measure/tstable.go
index ef191a4e0..c9054d541 100644
--- a/banyand/measure/tstable.go
+++ b/banyand/measure/tstable.go
@@ -112,6 +112,10 @@ func initTSTable(fileSystem fs.FileSystem, rootPath string, p common.Position,
for _, id := range loadedSnapshots {
fileSystem.MustRMAll(filepath.Join(rootPath, snapshotName(id)))
}
+ for _, id := range loadedParts {
+ l.Info().Str("path", partPath(rootPath, id)).Msg("delete orphaned part without snapshot")
+ fileSystem.MustRMAll(partPath(rootPath, id))
+ }
return &tst, uint64(time.Now().UnixNano())
}
sort.Slice(loadedSnapshots, func(i, j int) bool {
@@ -131,6 +135,10 @@ func initTSTable(fileSystem fs.FileSystem, rootPath string, p common.Position,
}
return &tst, epoch
}
+ for _, id := range loadedParts {
+ l.Info().Str("path", partPath(rootPath, id)).Msg("delete orphaned part after all snapshots failed to load")
+ fileSystem.MustRMAll(partPath(rootPath, id))
+ }
return &tst, uint64(time.Now().UnixNano())
}
diff --git a/banyand/stream/tstable.go b/banyand/stream/tstable.go
index dc4ea70ba..60ef8dcee 100644
--- a/banyand/stream/tstable.go
+++ b/banyand/stream/tstable.go
@@ -290,6 +290,10 @@ func initTSTable(fileSystem fs.FileSystem, rootPath string, p common.Position,
for _, id := range loadedSnapshots {
fileSystem.MustRMAll(filepath.Join(rootPath, snapshotName(id)))
}
+ for _, id := range loadedParts {
+ l.Info().Str("path", partPath(rootPath, id)).Msg("delete orphaned part without snapshot")
+ fileSystem.MustRMAll(partPath(rootPath, id))
+ }
return &tst, uint64(time.Now().UnixNano()), nil
}
sort.Slice(loadedSnapshots, func(i, j int) bool {
@@ -309,6 +313,10 @@ func initTSTable(fileSystem fs.FileSystem, rootPath string, p common.Position,
}
return &tst, epoch, nil
}
+ for _, id := range loadedParts {
+ l.Info().Str("path", partPath(rootPath, id)).Msg("delete orphaned part after all snapshots failed to load")
+ fileSystem.MustRMAll(partPath(rootPath, id))
+ }
return &tst, uint64(time.Now().UnixNano()), nil
}
diff --git a/banyand/trace/handoff_controller.go b/banyand/trace/handoff_controller.go
index bc45154f3..4e69f7559 100644
--- a/banyand/trace/handoff_controller.go
+++ b/banyand/trace/handoff_controller.go
@@ -223,8 +223,8 @@ func (hc *handoffController) enqueueForNode(nodeAddr string, partID uint64, part
// Read part size from metadata
partSize := hc.readPartSizeFromMetadata(sourcePath, partType)
- // Check if enqueue would exceed limit
- if !hc.canEnqueue(partSize) {
+ // Atomically check and reserve size to prevent TOCTOU race between check and update.
+ if !hc.tryReserveSize(partSize) {
currentSize := hc.getTotalSize()
return fmt.Errorf("handoff queue full: current=%d MB, limit=%d MB, part=%d MB",
currentSize/1024/1024, hc.maxTotalSizeBytes/1024/1024, partSize/1024/1024)
@@ -243,16 +243,15 @@ func (hc *handoffController) enqueueForNode(nodeAddr string, partID uint64, part
nodeQueue, err := hc.getOrCreateNodeQueue(nodeAddr)
if err != nil {
+ hc.updateTotalSize(-int64(partSize))
return fmt.Errorf("failed to get node queue for %s: %w", nodeAddr, err)
}
- if err := nodeQueue.enqueue(partID, partType, sourcePath, meta); err != nil {
- return err
+ if enqueueErr := nodeQueue.enqueue(partID, partType, sourcePath, meta); enqueueErr != nil {
+ hc.updateTotalSize(-int64(partSize))
+ return enqueueErr
}
- // Update total size after successful enqueue
- hc.updateTotalSize(int64(partSize))
-
return nil
}
@@ -714,15 +713,18 @@ func (hc *handoffController) stats() (partCount int64, totalSize int64) {
return count, int64(hc.getTotalSize())
}
-// canEnqueue checks if adding a part of the given size would exceed the total size limit.
-func (hc *handoffController) canEnqueue(partSize uint64) bool {
- if hc.maxTotalSizeBytes == 0 {
- return true // No limit configured
+// tryReserveSize atomically checks and reserves the given size if within the limit.
+// Returns true and increments currentTotalSize if the size can be reserved.
+// Returns false if adding partSize would exceed maxTotalSizeBytes.
+// Callers must call updateTotalSize(-int64(partSize)) to release on failure.
+func (hc *handoffController) tryReserveSize(partSize uint64) bool {
+ hc.sizeMu.Lock()
+ defer hc.sizeMu.Unlock()
+ if hc.maxTotalSizeBytes > 0 && hc.currentTotalSize+partSize > hc.maxTotalSizeBytes {
+ return false
}
-
- hc.sizeMu.RLock()
- defer hc.sizeMu.RUnlock()
- return hc.currentTotalSize+partSize <= hc.maxTotalSizeBytes
+ hc.currentTotalSize += partSize
+ return true
}
// readPartSizeFromMetadata reads the CompressedSizeBytes from the part's metadata file.
@@ -1210,28 +1212,77 @@ func (hc *handoffController) readPartFromHandoff(nodeAddr string, partID uint64,
PartType: partType,
}
- // For core parts, read additional metadata from metadata.json if present
- if partType == PartTypeCore {
- metadataPath := filepath.Join(partPath, metadataFilename)
- if metadataBytes, err := hc.fileSystem.Read(metadataPath); err == nil {
- var pm partMetadata
- if err := json.Unmarshal(metadataBytes, &pm); err == nil {
- streamingPart.CompressedSizeBytes = pm.CompressedSizeBytes
- streamingPart.UncompressedSizeBytes = pm.UncompressedSpanSizeBytes
- streamingPart.TotalCount = pm.TotalCount
- streamingPart.BlocksCount = pm.BlocksCount
- streamingPart.MinTimestamp = pm.MinTimestamp
- streamingPart.MaxTimestamp = pm.MaxTimestamp
- }
- }
- }
-
release := func() {
for _, buf := range buffers {
bigValuePool.Release(buf)
}
}
+ // For core parts, read additional metadata from metadata.json.
+ if partType == PartTypeCore {
+ metadataPath := filepath.Join(partPath, metadataFilename)
+ metadataBytes, readErr := hc.fileSystem.Read(metadataPath)
+ if readErr != nil {
+ release()
+ return nil, func() {}, fmt.Errorf("failed to read %s: %w", metadataFilename, readErr)
+ }
+ var pm partMetadata
+ if parseErr := json.Unmarshal(metadataBytes, &pm); parseErr != nil {
+ release()
+ return nil, func() {}, fmt.Errorf("failed to parse %s: %w", metadataFilename, parseErr)
+ }
+ streamingPart.CompressedSizeBytes = pm.CompressedSizeBytes
+ streamingPart.UncompressedSizeBytes = pm.UncompressedSpanSizeBytes
+ streamingPart.TotalCount = pm.TotalCount
+ streamingPart.BlocksCount = pm.BlocksCount
+ streamingPart.MinTimestamp = pm.MinTimestamp
+ streamingPart.MaxTimestamp = pm.MaxTimestamp
+ } else {
+ // For sidx parts, read manifest.json and populate timestamps using the same
+ // fallback logic as streaming sync: MinTimestamp pointer → SegmentID legacy → warn.
+ manifestPath := filepath.Join(partPath, "manifest.json")
+ manifestBytes, readErr := hc.fileSystem.Read(manifestPath)
+ if readErr != nil {
+ release()
+ return nil, func() {}, fmt.Errorf("failed to read manifest.json: %w", readErr)
+ }
+ var pm struct {
+ MinTimestamp *int64 `json:"minTimestamp,omitempty"`
+ MaxTimestamp *int64 `json:"maxTimestamp,omitempty"`
+ CompressedSizeBytes uint64 `json:"compressedSizeBytes"`
+ UncompressedSizeBytes uint64 `json:"uncompressedSizeBytes"`
+ TotalCount uint64 `json:"totalCount"`
+ BlocksCount uint64 `json:"blocksCount"`
+ MinKey int64 `json:"minKey"`
+ MaxKey int64 `json:"maxKey"`
+ SegmentID int64 `json:"segmentID"`
+ }
+ if parseErr := json.Unmarshal(manifestBytes, &pm); parseErr != nil {
+ release()
+ return nil, func() {}, fmt.Errorf("failed to parse manifest.json: %w", parseErr)
+ }
+ streamingPart.CompressedSizeBytes = pm.CompressedSizeBytes
+ streamingPart.UncompressedSizeBytes = pm.UncompressedSizeBytes
+ streamingPart.TotalCount = pm.TotalCount
+ streamingPart.BlocksCount = pm.BlocksCount
+ streamingPart.MinKey = pm.MinKey
+ streamingPart.MaxKey = pm.MaxKey
+ switch {
+ case pm.MinTimestamp != nil:
+ streamingPart.MinTimestamp = *pm.MinTimestamp
+ case pm.SegmentID > 0:
+ streamingPart.MinTimestamp = pm.SegmentID
+ default:
+ release()
+ return nil, func() {}, fmt.Errorf("sidx part %d has no valid timestamp (MinTimestamp=nil, SegmentID=0)", partID)
+ }
+ if pm.MaxTimestamp != nil {
+ streamingPart.MaxTimestamp = *pm.MaxTimestamp
+ } else {
+ streamingPart.MaxTimestamp = streamingPart.MinTimestamp
+ }
+ }
+
return streamingPart, release, nil
}
diff --git a/banyand/trace/handoff_replay_test.go b/banyand/trace/handoff_replay_test.go
index ec0610cdb..87335f2a1 100644
--- a/banyand/trace/handoff_replay_test.go
+++ b/banyand/trace/handoff_replay_test.go
@@ -404,3 +404,335 @@ func TestHandoffController_SendPartToNode(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 1, mockClient.getSendCount())
}
+
+// createTestSidxPart creates a minimal sidx part directory with the given manifest content.
+func createTestSidxPart(t *testing.T, fileSystem fs.FileSystem, root string, partID uint64, manifestContent []byte) string {
+ t.Helper()
+ partPath := filepath.Join(root, partName(partID))
+ fileSystem.MkdirIfNotExist(partPath, storage.DirPerm)
+
+ sidxFiles := map[string][]byte{
+ "primary.bin": []byte("test primary data"),
+ "data.bin": []byte("test data"),
+ "keys.bin": []byte("test keys"),
+ "meta.bin": []byte("test meta"),
+ }
+ if manifestContent != nil {
+ sidxFiles["manifest.json"] = manifestContent
+ }
+
+ for filename, content := range sidxFiles {
+ filePath := filepath.Join(partPath, filename)
+ lf, err := fileSystem.CreateLockFile(filePath, storage.FilePerm)
+ require.NoError(t, err)
+ _, err = lf.Write(content)
+ require.NoError(t, err)
+ require.NoError(t, lf.Close())
+ }
+
+ return partPath
+}
+
+func TestHandoffController_ReadPartFromHandoff_CoreMetadata(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x60)
+ partPath := filepath.Join(sourceRoot, partName(partID))
+ fileSystem.MkdirIfNotExist(partPath, storage.DirPerm)
+
+ coreFiles := map[string][]byte{
+ "primary.bin": []byte("primary data"),
+ "spans.bin": []byte("spans data"),
+ "meta.bin": []byte("meta data"),
+ "metadata.json": []byte(`{"compressedSizeBytes":1024,"uncompressedSpanSizeBytes":2048,` +
+ `"totalCount":50,"blocksCount":5,"minTimestamp":1700000000,"maxTimestamp":1700001000}`),
+ "tag.type": []byte("tag type"),
+ "traceID.filter": []byte("filter"),
+ }
+ for filename, content := range coreFiles {
+ filePath := filepath.Join(partPath, filename)
+ lf, err := fileSystem.CreateLockFile(filePath, storage.FilePerm)
+ require.NoError(t, err)
+ _, err = lf.Write(content)
+ require.NoError(t, err)
+ require.NoError(t, lf.Close())
+ }
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, PartTypeCore, partPath, "group1", 1))
+
+ streamingPart, release, err := controller.readPartFromHandoff(nodeAddr, partID, PartTypeCore)
+ require.NoError(t, err)
+ defer release()
+
+ assert.Equal(t, uint64(1024), streamingPart.CompressedSizeBytes)
+ assert.Equal(t, uint64(2048), streamingPart.UncompressedSizeBytes)
+ assert.Equal(t, uint64(50), streamingPart.TotalCount)
+ assert.Equal(t, uint64(5), streamingPart.BlocksCount)
+ assert.Equal(t, int64(1700000000), streamingPart.MinTimestamp)
+ assert.Equal(t, int64(1700001000), streamingPart.MaxTimestamp)
+}
+
+func TestHandoffController_ReadPartFromHandoff_CoreMissingMetadata(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x61)
+ partPath := filepath.Join(sourceRoot, partName(partID))
+ fileSystem.MkdirIfNotExist(partPath, storage.DirPerm)
+
+ coreFiles := map[string][]byte{
+ "primary.bin": []byte("primary data"),
+ "spans.bin": []byte("spans data"),
+ "meta.bin": []byte("meta data"),
+ }
+ for filename, content := range coreFiles {
+ filePath := filepath.Join(partPath, filename)
+ lf, err := fileSystem.CreateLockFile(filePath, storage.FilePerm)
+ require.NoError(t, err)
+ _, err = lf.Write(content)
+ require.NoError(t, err)
+ require.NoError(t, lf.Close())
+ }
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, PartTypeCore, partPath, "group1", 1))
+
+ _, _, readErr := controller.readPartFromHandoff(nodeAddr, partID, PartTypeCore)
+ require.Error(t, readErr)
+ assert.Contains(t, readErr.Error(), "failed to read metadata.json")
+}
+
+func TestHandoffController_ReadPartFromHandoff_CoreInvalidMetadata(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x62)
+ partPath := filepath.Join(sourceRoot, partName(partID))
+ fileSystem.MkdirIfNotExist(partPath, storage.DirPerm)
+
+ coreFiles := map[string][]byte{
+ "primary.bin": []byte("primary data"),
+ "spans.bin": []byte("spans data"),
+ "meta.bin": []byte("meta data"),
+ "metadata.json": []byte(`{invalid json`),
+ }
+ for filename, content := range coreFiles {
+ filePath := filepath.Join(partPath, filename)
+ lf, err := fileSystem.CreateLockFile(filePath, storage.FilePerm)
+ require.NoError(t, err)
+ _, err = lf.Write(content)
+ require.NoError(t, err)
+ require.NoError(t, lf.Close())
+ }
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, PartTypeCore, partPath, "group1", 1))
+
+ _, _, readErr := controller.readPartFromHandoff(nodeAddr, partID, PartTypeCore)
+ require.Error(t, readErr)
+ assert.Contains(t, readErr.Error(), "failed to parse metadata.json")
+}
+
+func TestHandoffController_ReadPartFromHandoff_SidxWithTimestamps(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x70)
+
+ manifest := []byte(`{
+ "minTimestamp": 1700000000,
+ "maxTimestamp": 1700001000,
+ "compressedSizeBytes": 512,
+ "uncompressedSizeBytes": 1024,
+ "totalCount": 20,
+ "blocksCount": 2,
+ "minKey": 10,
+ "maxKey": 200,
+ "segmentID": 1700099999
+ }`)
+ sourcePath := createTestSidxPart(t, fileSystem, sourceRoot, partID, manifest)
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, "sidx_trace_id", sourcePath, "group1", 1))
+
+ streamingPart, release, err := controller.readPartFromHandoff(nodeAddr, partID, "sidx_trace_id")
+ require.NoError(t, err)
+ defer release()
+
+ assert.Equal(t, partID, streamingPart.ID)
+ assert.Equal(t, "group1", streamingPart.Group)
+ assert.Equal(t, "sidx_trace_id", streamingPart.PartType)
+ assert.Equal(t, uint64(512), streamingPart.CompressedSizeBytes)
+ assert.Equal(t, uint64(1024), streamingPart.UncompressedSizeBytes)
+ assert.Equal(t, uint64(20), streamingPart.TotalCount)
+ assert.Equal(t, uint64(2), streamingPart.BlocksCount)
+ assert.Equal(t, int64(10), streamingPart.MinKey)
+ assert.Equal(t, int64(200), streamingPart.MaxKey)
+ // MinTimestamp comes from the pointer field, not SegmentID
+ assert.Equal(t, int64(1700000000), streamingPart.MinTimestamp)
+ assert.Equal(t, int64(1700001000), streamingPart.MaxTimestamp)
+}
+
+func TestHandoffController_ReadPartFromHandoff_SidxWithSegmentIDFallback(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x71)
+
+ // Legacy manifest: no minTimestamp/maxTimestamp, only segmentID
+ manifest := []byte(`{
+ "compressedSizeBytes": 256,
+ "uncompressedSizeBytes": 512,
+ "totalCount": 10,
+ "blocksCount": 1,
+ "minKey": 5,
+ "maxKey": 100,
+ "segmentID": 1700050000
+ }`)
+ sourcePath := createTestSidxPart(t, fileSystem, sourceRoot, partID, manifest)
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, "sidx_trace_id", sourcePath, "group1", 1))
+
+ streamingPart, release, err := controller.readPartFromHandoff(nodeAddr, partID, "sidx_trace_id")
+ require.NoError(t, err)
+ defer release()
+
+ // MinTimestamp falls back to SegmentID; MaxTimestamp falls back to MinTimestamp
+ assert.Equal(t, int64(1700050000), streamingPart.MinTimestamp)
+ assert.Equal(t, int64(1700050000), streamingPart.MaxTimestamp)
+ assert.Equal(t, uint64(256), streamingPart.CompressedSizeBytes)
+}
+
+func TestHandoffController_ReadPartFromHandoff_SidxMissingManifest(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x72)
+
+ // Create sidx part without manifest.json
+ sourcePath := createTestSidxPart(t, fileSystem, sourceRoot, partID, nil)
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, "sidx_trace_id", sourcePath, "group1", 1))
+
+ _, _, readErr := controller.readPartFromHandoff(nodeAddr, partID, "sidx_trace_id")
+ require.Error(t, readErr)
+ assert.Contains(t, readErr.Error(), "failed to read manifest.json")
+}
+
+func TestHandoffController_ReadPartFromHandoff_SidxInvalidManifest(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x73)
+
+ manifest := []byte(`{broken json`)
+ sourcePath := createTestSidxPart(t, fileSystem, sourceRoot, partID, manifest)
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, "sidx_trace_id", sourcePath, "group1", 1))
+
+ _, _, readErr := controller.readPartFromHandoff(nodeAddr, partID, "sidx_trace_id")
+ require.Error(t, readErr)
+ assert.Contains(t, readErr.Error(), "failed to parse manifest.json")
+}
+
+func TestHandoffController_ReadPartFromHandoff_SidxNoValidTimestamp(t *testing.T) {
+ tempDir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+
+ sourceRoot := filepath.Join(tempDir, "source")
+ fileSystem.MkdirIfNotExist(sourceRoot, storage.DirPerm)
+ partID := uint64(0x74)
+
+ // Manifest with no minTimestamp and segmentID=0 (invalid)
+ manifest := []byte(`{
+ "compressedSizeBytes": 128,
+ "totalCount": 5,
+ "blocksCount": 1,
+ "minKey": 1,
+ "maxKey": 10,
+ "segmentID": 0
+ }`)
+ sourcePath := createTestSidxPart(t, fileSystem, sourceRoot, partID, manifest)
+
+ nodeAddr := testNodeAddrPrimary
+ controller, err := newHandoffController(fileSystem, tempDir, nil, []string{nodeAddr}, 0, l, nil)
+ require.NoError(t, err)
+ defer controller.close()
+
+ require.NoError(t, controller.enqueueForNode(nodeAddr, partID, "sidx_trace_id", sourcePath, "group1", 1))
+
+ _, _, readErr := controller.readPartFromHandoff(nodeAddr, partID, "sidx_trace_id")
+ require.Error(t, readErr)
+ assert.Contains(t, readErr.Error(), "has no valid timestamp")
+}
diff --git a/banyand/trace/handoff_storage_test.go b/banyand/trace/handoff_storage_test.go
index 93084afff..4ca00ca8f 100644
--- a/banyand/trace/handoff_storage_test.go
+++ b/banyand/trace/handoff_storage_test.go
@@ -22,6 +22,7 @@ import (
"errors"
"os"
"path/filepath"
+ "sync"
"testing"
"time"
@@ -690,3 +691,58 @@ func TestHandoffController_SizeRecovery(t *testing.T) {
tester.NoError(err)
tester.Len(node2Pending, 1)
}
+
+// TestHandoffController_ConcurrentSizeEnforcement verifies that concurrent enqueues
+// cannot exceed the size limit due to the atomic tryReserveSize check.
+func TestHandoffController_ConcurrentSizeEnforcement(t *testing.T) {
+ tester := require.New(t)
+ tempDir, deferFunc := test.Space(tester)
+ defer deferFunc()
+
+ // Create a source part with 3MB metadata
+ partID := uint64(500)
+ partPath := filepath.Join(tempDir, "source", partName(partID))
+ tester.NoError(os.MkdirAll(partPath, 0o755))
+
+ metadata := map[string]interface{}{
+ "compressedSizeBytes": 3 * megabyte,
+ }
+ metadataBytes, err := json.Marshal(metadata)
+ tester.NoError(err)
+ tester.NoError(os.WriteFile(filepath.Join(partPath, "metadata.json"), metadataBytes, 0o600))
+ tester.NoError(os.WriteFile(filepath.Join(partPath, "data.bin"), []byte("test data"), 0o600))
+
+ // 10MB limit allows at most 3 parts of 3MB each
+ lfs := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+ hc, err := newHandoffController(lfs, tempDir, nil, []string{"node1:17912"}, 10*megabyte, l, nil)
+ tester.NoError(err)
+ defer hc.close()
+
+ const numGoroutines = 20
+ var wg sync.WaitGroup
+ var successCount int64
+ var mu sync.Mutex
+
+ for i := 0; i < numGoroutines; i++ {
+ wg.Add(1)
+ go func(idx int) {
+ defer wg.Done()
+ enqueueErr := hc.enqueueForNode("node1:17912", partID+uint64(idx), PartTypeCore, partPath, "group1", 1)
+ if enqueueErr == nil {
+ mu.Lock()
+ successCount++
+ mu.Unlock()
+ }
+ }(i)
+ }
+ wg.Wait()
+
+ // At most 3 enqueues should succeed (3 * 3MB = 9MB <= 10MB)
+ tester.LessOrEqual(successCount, int64(3))
+
+ // Total size must not exceed 10MB limit
+ totalSize := hc.getTotalSize()
+ tester.LessOrEqual(totalSize, uint64(10*megabyte))
+ tester.Equal(uint64(successCount)*3*megabyte, totalSize)
+}
diff --git a/banyand/trace/tstable.go b/banyand/trace/tstable.go
index dcb68f9b9..741f1221f 100644
--- a/banyand/trace/tstable.go
+++ b/banyand/trace/tstable.go
@@ -279,6 +279,10 @@ func initTSTable(fileSystem fs.FileSystem, rootPath string, p common.Position,
for _, id := range loadedSnapshots {
fileSystem.MustRMAll(filepath.Join(rootPath, snapshotName(id)))
}
+ for _, id := range loadedParts {
+ l.Info().Str("path", partPath(rootPath, id)).Msg("delete orphaned part without snapshot")
+ fileSystem.MustRMAll(partPath(rootPath, id))
+ }
return &tst, uint64(time.Now().UnixNano())
}
sort.Slice(loadedSnapshots, func(i, j int) bool {
@@ -298,6 +302,10 @@ func initTSTable(fileSystem fs.FileSystem, rootPath string, p common.Position,
}
return &tst, epoch
}
+ for _, id := range loadedParts {
+ l.Info().Str("path", partPath(rootPath, id)).Msg("delete orphaned part after all snapshots failed to load")
+ fileSystem.MustRMAll(partPath(rootPath, id))
+ }
return &tst, uint64(time.Now().UnixNano())
}
diff --git a/test/integration/handoff/handoff_suite_test.go b/test/integration/handoff/handoff_suite_test.go
index 142a931b7..e6170fd09 100644
--- a/test/integration/handoff/handoff_suite_test.go
+++ b/test/integration/handoff/handoff_suite_test.go
@@ -421,6 +421,11 @@ var _ = Describe("trace handoff", func() {
return queryTrace(connection, traceID, writeTime)
}, flags.EventuallyTimeout).Should(Succeed())
+ By("verifying sidx data was replayed correctly by querying via indexed tag")
+ Eventually(func() error {
+ return queryTraceByService(connection, "handoff_service", writeTime)
+ }, flags.EventuallyTimeout).Should(Succeed())
+
var otherIndex int
for idx := range dnHandles {
if idx != targetIndex {
@@ -586,3 +591,42 @@ func waitForPendingParts(nodeAddr string, timeout time.Duration) bool {
time.Sleep(100 * time.Millisecond)
}
}
+
+// queryTraceByService queries traces by service_id, which exercises the sidx secondary index.
+// If sidx parts were replayed with MinTimestamp=0, the sidx data is rejected or corrupted,
+// and this query fails to find traces even though core data exists.
+func queryTraceByService(conn *grpc.ClientConn, serviceID string, ts time.Time) error {
+ client := tracev1.NewTraceServiceClient(conn)
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ req := &tracev1.QueryRequest{
+ Groups: []string{"test-trace-group"},
+ Name: "sw",
+ TimeRange: &modelv1.TimeRange{
+ Begin: timestamppb.New(ts.Add(-5 * time.Minute)),
+ End: timestamppb.New(ts.Add(5 * time.Minute)),
+ },
+ Criteria: &modelv1.Criteria{
+ Exp: &modelv1.Criteria_Condition{
+ Condition: &modelv1.Condition{
+ Name: "service_id",
+ Op: modelv1.Condition_BINARY_OP_EQ,
+ Value: &modelv1.TagValue{Value: &modelv1.TagValue_Str{
+ Str: &modelv1.Str{Value: serviceID},
+ }},
+ },
+ },
+ },
+ TagProjection: []string{"trace_id", "service_id"},
+ }
+
+ resp, queryErr := client.Query(ctx, req)
+ if queryErr != nil {
+ return queryErr
+ }
+ if len(resp.GetTraces()) == 0 {
+ return fmt.Errorf("no traces found for service_id=%s", serviceID)
+ }
+ return nil
+}
(skywalking-banyandb) 22/24: chore: bump ui and mcp npm dependencies (#1104)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit c31232e23d20a6fae272facdb67772d718ed551d
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Wed Apr 29 15:28:46 2026 +0800
chore: bump ui and mcp npm dependencies (#1104)
---
dist/LICENSE | 46 +--
.../mcp-licenses/license-eventsource-parser.txt | 2 +-
mcp/LICENSE | 14 +-
mcp/package-lock.json | 240 ++++++------
ui/LICENSE | 32 +-
ui/package-lock.json | 409 +++++++++++----------
6 files changed, 384 insertions(+), 359 deletions(-)
diff --git a/dist/LICENSE b/dist/LICENSE
index 25bdabdce..45ae5033b 100644
--- a/dist/LICENSE
+++ b/dist/LICENSE
@@ -529,21 +529,21 @@ MIT licenses
@parcel/watcher-linux-x64-musl 2.5.6 MIT
@popperjs/core 2.11.8 MIT
@tanstack/match-sorter-utils 8.19.4 MIT
- @tanstack/query-core 5.97.0 MIT
- @tanstack/vue-query 5.97.0 MIT
+ @tanstack/query-core 5.100.6 MIT
+ @tanstack/vue-query 5.100.6 MIT
@types/lodash 4.17.24 MIT
@types/lodash-es 4.17.12 MIT
@types/web-bluetooth 0.0.20 MIT
- @vue/compiler-core 3.5.32 MIT
- @vue/compiler-dom 3.5.32 MIT
- @vue/compiler-sfc 3.5.32 MIT
- @vue/compiler-ssr 3.5.32 MIT
+ @vue/compiler-core 3.5.33 MIT
+ @vue/compiler-dom 3.5.33 MIT
+ @vue/compiler-sfc 3.5.33 MIT
+ @vue/compiler-ssr 3.5.33 MIT
@vue/devtools-api 6.6.4 MIT
- @vue/reactivity 3.5.32 MIT
- @vue/runtime-core 3.5.32 MIT
- @vue/runtime-dom 3.5.32 MIT
- @vue/server-renderer 3.5.32 MIT
- @vue/shared 3.5.32 MIT
+ @vue/reactivity 3.5.33 MIT
+ @vue/runtime-core 3.5.33 MIT
+ @vue/runtime-dom 3.5.33 MIT
+ @vue/server-renderer 3.5.33 MIT
+ @vue/shared 3.5.33 MIT
@vueuse/core 12.0.0 MIT
@vueuse/metadata 12.0.0 MIT
@vueuse/shared 12.0.0 MIT
@@ -553,7 +553,7 @@ MIT licenses
core-util-is 1.0.3 MIT
csstype 3.2.3 MIT
dayjs 1.11.20 MIT
- element-plus 2.13.6 MIT
+ element-plus 2.13.7 MIT
estree-walker 2.0.2 MIT
immediate 3.0.6 MIT
immutable 5.1.5 MIT
@@ -561,7 +561,7 @@ MIT licenses
is-glob 4.0.3 MIT
isarray 1.0.0 MIT
js-yaml 4.1.1 MIT
- jsonfile 6.2.0 MIT
+ jsonfile 6.2.1 MIT
lie 3.3.0 MIT
lodash 4.17.23 MIT
lodash-es 4.17.23 MIT
@@ -573,7 +573,7 @@ MIT licenses
node-addon-api 7.1.1 MIT
picomatch 4.0.4 MIT
pinia 2.3.1 MIT
- postcss 8.5.9 MIT
+ postcss 8.5.12 MIT
process-nextick-args 2.0.1 MIT
readable-stream 2.3.8 MIT
readdirp 4.1.2 MIT
@@ -584,8 +584,8 @@ MIT licenses
string_decoder 1.1.1 MIT
universalify 2.0.1 MIT
util-deprecate 1.0.2 MIT
- vue 3.5.32 MIT
- vue-component-type-helpers 3.2.6 MIT
+ vue 3.5.33 MIT
+ vue-component-type-helpers 3.2.7 MIT
vue-demi 0.14.10 MIT
vue-router 4.6.4 MIT
@@ -650,10 +650,10 @@ ISC licenses
MIT licenses
========================================================================
- @hono/node-server 1.19.13 MIT
+ @hono/node-server 1.19.14 MIT
@modelcontextprotocol/sdk 1.29.0 MIT
accepts 2.0.0 MIT
- ajv 8.18.0 MIT
+ ajv 8.20.0 MIT
ajv-formats 3.0.1 MIT
body-parser 2.2.2 MIT
bytes 3.1.2 MIT
@@ -676,9 +676,9 @@ MIT licenses
escape-html 1.0.3 MIT
etag 1.8.1 MIT
eventsource 3.0.7 MIT
- eventsource-parser 3.0.6 MIT
+ eventsource-parser 3.0.8 MIT
express 5.2.1 MIT
- express-rate-limit 8.3.2 MIT
+ express-rate-limit 8.4.1 MIT
fast-deep-equal 3.1.3 MIT
finalhandler 2.1.1 MIT
forwarded 0.2.0 MIT
@@ -688,14 +688,14 @@ MIT licenses
get-proto 1.0.1 MIT
gopd 1.2.0 MIT
has-symbols 1.1.0 MIT
- hasown 2.0.2 MIT
- hono 4.12.12 MIT
+ hasown 2.0.3 MIT
+ hono 4.12.15 MIT
http-errors 2.0.1 MIT
iconv-lite 0.7.2 MIT
ip-address 10.1.0 MIT
ipaddr.js 1.9.1 MIT
is-promise 4.0.0 MIT
- jose 6.2.2 MIT
+ jose 6.2.3 MIT
json-schema-traverse 1.0.0 MIT
math-intrinsics 1.1.0 MIT
media-typer 1.1.0 MIT
diff --git a/dist/licenses/mcp-licenses/license-eventsource-parser.txt b/dist/licenses/mcp-licenses/license-eventsource-parser.txt
index 82f30788b..747ce3cf5 100644
--- a/dist/licenses/mcp-licenses/license-eventsource-parser.txt
+++ b/dist/licenses/mcp-licenses/license-eventsource-parser.txt
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2025 Espen Hovlandsdal <es...@hovlandsdal.com>
+Copyright (c) 2026 Espen Hovlandsdal <es...@hovlandsdal.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/mcp/LICENSE b/mcp/LICENSE
index 46b91819a..1343e0c77 100644
--- a/mcp/LICENSE
+++ b/mcp/LICENSE
@@ -54,10 +54,10 @@ ISC licenses
MIT licenses
========================================================================
- @hono/node-server 1.19.13 MIT
+ @hono/node-server 1.19.14 MIT
@modelcontextprotocol/sdk 1.29.0 MIT
accepts 2.0.0 MIT
- ajv 8.18.0 MIT
+ ajv 8.20.0 MIT
ajv-formats 3.0.1 MIT
body-parser 2.2.2 MIT
bytes 3.1.2 MIT
@@ -80,9 +80,9 @@ MIT licenses
escape-html 1.0.3 MIT
etag 1.8.1 MIT
eventsource 3.0.7 MIT
- eventsource-parser 3.0.6 MIT
+ eventsource-parser 3.0.8 MIT
express 5.2.1 MIT
- express-rate-limit 8.3.2 MIT
+ express-rate-limit 8.4.1 MIT
fast-deep-equal 3.1.3 MIT
finalhandler 2.1.1 MIT
forwarded 0.2.0 MIT
@@ -92,14 +92,14 @@ MIT licenses
get-proto 1.0.1 MIT
gopd 1.2.0 MIT
has-symbols 1.1.0 MIT
- hasown 2.0.2 MIT
- hono 4.12.12 MIT
+ hasown 2.0.3 MIT
+ hono 4.12.15 MIT
http-errors 2.0.1 MIT
iconv-lite 0.7.2 MIT
ip-address 10.1.0 MIT
ipaddr.js 1.9.1 MIT
is-promise 4.0.0 MIT
- jose 6.2.2 MIT
+ jose 6.2.3 MIT
json-schema-traverse 1.0.0 MIT
math-intrinsics 1.1.0 MIT
media-typer 1.1.0 MIT
diff --git a/mcp/package-lock.json b/mcp/package-lock.json
index 7ea6249cc..199ecf997 100644
--- a/mcp/package-lock.json
+++ b/mcp/package-lock.json
@@ -523,9 +523,9 @@
"license": "MIT"
},
"node_modules/@eslint/config-array/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -597,9 +597,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -621,9 +621,9 @@
"license": "MIT"
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -699,9 +699,9 @@
}
},
"node_modules/@hono/node-server": {
- "version": "1.19.13",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
- "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
+ "version": "1.19.14",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
+ "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -711,29 +711,43 @@
}
},
"node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
+ "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"dev": true,
"license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/types": "^0.15.0"
+ },
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
- "version": "0.16.7",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
- "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "version": "0.16.8",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
+ "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@humanfs/core": "^0.19.1",
+ "@humanfs/core": "^0.19.2",
+ "@humanfs/types": "^0.15.0",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
+ "node_modules/@humanfs/types": {
+ "version": "0.15.0",
+ "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
+ "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -827,17 +841,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
- "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.1.tgz",
+ "integrity": "sha512-BOziFIfE+6osHO9FoJG4zjoHUcvI7fTNBSpdAwrNH0/TLvzjsk2oo8XSSOT2HhqUyhZPfHv4UOffoJ9oEEQ7Ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.58.1",
- "@typescript-eslint/type-utils": "8.58.1",
- "@typescript-eslint/utils": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/type-utils": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -850,22 +864,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.58.1",
+ "@typescript-eslint/parser": "^8.59.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
- "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.1.tgz",
+ "integrity": "sha512-HDQH9O/47Dxi1ceDhBXdaldtf/WV9yRYMjbjCuNk3qnaTD564qwv61Y7+gTxwxRKzSrgO5uhtw584igXVuuZkA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.58.1",
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1",
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
"debug": "^4.4.3"
},
"engines": {
@@ -881,14 +895,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
- "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.1.tgz",
+ "integrity": "sha512-+MuHQlHiEr00Of/IQbE/MmEoi44znZHbR/Pz7Opq4HryUOlRi+/44dro9Ycy8Fyo+/024IWtw8m4JUMCGTYxDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.58.1",
- "@typescript-eslint/types": "^8.58.1",
+ "@typescript-eslint/tsconfig-utils": "^8.59.1",
+ "@typescript-eslint/types": "^8.59.1",
"debug": "^4.4.3"
},
"engines": {
@@ -903,14 +917,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
- "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.1.tgz",
+ "integrity": "sha512-LwuHQI4pDOYVKvmH2dkaJo6YZCSgouVgnS/z7yBPKBMvgtBvyLqiLy9Z6b7+m/TRcX1NFYUqZetI5Y+aT4GEfg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1"
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -921,9 +935,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
- "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.1.tgz",
+ "integrity": "sha512-/0nEyPbX7gRsk0Uwfe4ALwwgxuA66d/l2mhRDNlAvaj4U3juhUtJNq0DsY8M2AYwwb9rEq2hrC3IcIcEt++iJA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -938,15 +952,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
- "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.1.tgz",
+ "integrity": "sha512-klWPBR2ciQHS3f++ug/mVnWKPjBUo7icEL3FAO1lhAR1Z1i5NQYZ1EannMSRYcq5qCv5wNALlXr6fksRHyYl7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1",
- "@typescript-eslint/utils": "8.58.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -963,9 +977,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
- "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.1.tgz",
+ "integrity": "sha512-ZDCjgccSdYPw5Bxh+my4Z0lJU96ZDN7jbBzvmEn0FZx3RtU1C7VWl6NbDx94bwY3V5YsgwRzJPOgeY2Q/nLG8A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -977,16 +991,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
- "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.1.tgz",
+ "integrity": "sha512-OUd+vJS05sSkOip+BkZ/2NS8RMxrAAJemsC6vU3kmfLyeaJT0TftHkV9mcx2107MmsBVXXexhVu4F0TZXyMl4g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.58.1",
- "@typescript-eslint/tsconfig-utils": "8.58.1",
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/visitor-keys": "8.58.1",
+ "@typescript-eslint/project-service": "8.59.1",
+ "@typescript-eslint/tsconfig-utils": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/visitor-keys": "8.59.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1005,16 +1019,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
- "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.1.tgz",
+ "integrity": "sha512-3pIeoXhCeYH9FSCBI8P3iNwJlGuzPlYKkTlen2O9T1DSeeg8UG8jstq6BLk+Mda0qup7mgk4z4XL4OzRaxZ8LA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.58.1",
- "@typescript-eslint/types": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1"
+ "@typescript-eslint/scope-manager": "8.59.1",
+ "@typescript-eslint/types": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1029,13 +1043,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
- "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.1.tgz",
+ "integrity": "sha512-LdDNl6C5iJExcM0Yh0PwAIBb9PrSiCsWamF/JyEZawm3kFDnRoaq3LGE4bpyRao/fWeGKKyw7icx0YxrLFC5Cg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/types": "8.59.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1096,9 +1110,9 @@
}
},
"node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -1617,9 +1631,9 @@
}
},
"node_modules/eslint/node_modules/ajv": {
- "version": "6.14.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
- "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
+ "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1641,9 +1655,9 @@
"license": "MIT"
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.13",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
- "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "version": "1.1.14",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
+ "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1793,9 +1807,9 @@
}
},
"node_modules/eventsource-parser": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz",
- "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==",
+ "version": "3.0.8",
+ "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
+ "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
@@ -1845,9 +1859,9 @@
}
},
"node_modules/express-rate-limit": {
- "version": "8.3.2",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
- "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
+ "version": "8.4.1",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.4.1.tgz",
+ "integrity": "sha512-NGVYwQSAyEQgzxX1iCM978PP9AdO/hW93gMcF6ZwQCm+rFvLsBH6w4xcXWTcliS8La5EPRN3p9wzItqBwJrfNw==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
@@ -2068,9 +2082,9 @@
}
},
"node_modules/get-tsconfig": {
- "version": "4.13.7",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz",
- "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==",
+ "version": "4.14.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
+ "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2149,9 +2163,9 @@
}
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
@@ -2161,9 +2175,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.12",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
- "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
+ "version": "4.12.15",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.15.tgz",
+ "integrity": "sha512-qM0jDhFEaCBb4TxoW7f53Qrpv9RBiayUHo0S52JudprkhvpjIrGoU1mnnr29Fvd1U335ZFPZQY1wlkqgfGXyLg==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -2302,9 +2316,9 @@
"license": "ISC"
},
"node_modules/jose": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz",
- "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==",
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
+ "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
@@ -2350,9 +2364,9 @@
"license": "MIT"
},
"node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2678,9 +2692,9 @@
}
},
"node_modules/prettier": {
- "version": "3.8.1",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
- "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -3099,16 +3113,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.58.1",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz",
- "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==",
+ "version": "8.59.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.1.tgz",
+ "integrity": "sha512-xqDcFVBmlrltH64lklOVp1wYxgJr6LVdg3NamBgH2OOQDLFdTKfIZXF5PfghrnXQKXZGTQs8tr1vL7fJvq8CTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.58.1",
- "@typescript-eslint/parser": "8.58.1",
- "@typescript-eslint/typescript-estree": "8.58.1",
- "@typescript-eslint/utils": "8.58.1"
+ "@typescript-eslint/eslint-plugin": "8.59.1",
+ "@typescript-eslint/parser": "8.59.1",
+ "@typescript-eslint/typescript-estree": "8.59.1",
+ "@typescript-eslint/utils": "8.59.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
diff --git a/ui/LICENSE b/ui/LICENSE
index 1e0722d87..b0d5014f6 100644
--- a/ui/LICENSE
+++ b/ui/LICENSE
@@ -74,21 +74,21 @@ MIT licenses
@parcel/watcher-linux-x64-musl 2.5.6 MIT
@popperjs/core 2.11.8 MIT
@tanstack/match-sorter-utils 8.19.4 MIT
- @tanstack/query-core 5.97.0 MIT
- @tanstack/vue-query 5.97.0 MIT
+ @tanstack/query-core 5.100.6 MIT
+ @tanstack/vue-query 5.100.6 MIT
@types/lodash 4.17.24 MIT
@types/lodash-es 4.17.12 MIT
@types/web-bluetooth 0.0.20 MIT
- @vue/compiler-core 3.5.32 MIT
- @vue/compiler-dom 3.5.32 MIT
- @vue/compiler-sfc 3.5.32 MIT
- @vue/compiler-ssr 3.5.32 MIT
+ @vue/compiler-core 3.5.33 MIT
+ @vue/compiler-dom 3.5.33 MIT
+ @vue/compiler-sfc 3.5.33 MIT
+ @vue/compiler-ssr 3.5.33 MIT
@vue/devtools-api 6.6.4 MIT
- @vue/reactivity 3.5.32 MIT
- @vue/runtime-core 3.5.32 MIT
- @vue/runtime-dom 3.5.32 MIT
- @vue/server-renderer 3.5.32 MIT
- @vue/shared 3.5.32 MIT
+ @vue/reactivity 3.5.33 MIT
+ @vue/runtime-core 3.5.33 MIT
+ @vue/runtime-dom 3.5.33 MIT
+ @vue/server-renderer 3.5.33 MIT
+ @vue/shared 3.5.33 MIT
@vueuse/core 12.0.0 MIT
@vueuse/metadata 12.0.0 MIT
@vueuse/shared 12.0.0 MIT
@@ -98,7 +98,7 @@ MIT licenses
core-util-is 1.0.3 MIT
csstype 3.2.3 MIT
dayjs 1.11.20 MIT
- element-plus 2.13.6 MIT
+ element-plus 2.13.7 MIT
estree-walker 2.0.2 MIT
immediate 3.0.6 MIT
immutable 5.1.5 MIT
@@ -106,7 +106,7 @@ MIT licenses
is-glob 4.0.3 MIT
isarray 1.0.0 MIT
js-yaml 4.1.1 MIT
- jsonfile 6.2.0 MIT
+ jsonfile 6.2.1 MIT
lie 3.3.0 MIT
lodash 4.17.23 MIT
lodash-es 4.17.23 MIT
@@ -118,7 +118,7 @@ MIT licenses
node-addon-api 7.1.1 MIT
picomatch 4.0.4 MIT
pinia 2.3.1 MIT
- postcss 8.5.9 MIT
+ postcss 8.5.12 MIT
process-nextick-args 2.0.1 MIT
readable-stream 2.3.8 MIT
readdirp 4.1.2 MIT
@@ -129,8 +129,8 @@ MIT licenses
string_decoder 1.1.1 MIT
universalify 2.0.1 MIT
util-deprecate 1.0.2 MIT
- vue 3.5.32 MIT
- vue-component-type-helpers 3.2.6 MIT
+ vue 3.5.33 MIT
+ vue-component-type-helpers 3.2.7 MIT
vue-demi 0.14.10 MIT
vue-router 4.6.4 MIT
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 64e6259ff..d007068c3 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -951,9 +951,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
- "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
+ "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==",
"cpu": [
"arm"
],
@@ -965,9 +965,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
- "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz",
+ "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==",
"cpu": [
"arm64"
],
@@ -979,9 +979,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
- "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz",
+ "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==",
"cpu": [
"arm64"
],
@@ -993,9 +993,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
- "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz",
+ "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==",
"cpu": [
"x64"
],
@@ -1007,9 +1007,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
- "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz",
+ "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==",
"cpu": [
"arm64"
],
@@ -1021,9 +1021,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
- "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz",
+ "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==",
"cpu": [
"x64"
],
@@ -1035,9 +1035,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
- "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz",
+ "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==",
"cpu": [
"arm"
],
@@ -1049,9 +1049,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
- "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz",
+ "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==",
"cpu": [
"arm"
],
@@ -1063,9 +1063,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
- "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz",
+ "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==",
"cpu": [
"arm64"
],
@@ -1077,9 +1077,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
- "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz",
+ "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==",
"cpu": [
"arm64"
],
@@ -1091,9 +1091,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
- "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz",
+ "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==",
"cpu": [
"loong64"
],
@@ -1105,9 +1105,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
- "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz",
+ "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==",
"cpu": [
"loong64"
],
@@ -1119,9 +1119,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
- "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz",
+ "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==",
"cpu": [
"ppc64"
],
@@ -1133,9 +1133,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
- "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz",
+ "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==",
"cpu": [
"ppc64"
],
@@ -1147,9 +1147,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
- "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz",
+ "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==",
"cpu": [
"riscv64"
],
@@ -1161,9 +1161,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
- "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz",
+ "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==",
"cpu": [
"riscv64"
],
@@ -1175,9 +1175,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
- "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz",
+ "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==",
"cpu": [
"s390x"
],
@@ -1189,9 +1189,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
- "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==",
"cpu": [
"x64"
],
@@ -1203,9 +1203,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
- "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
+ "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
"cpu": [
"x64"
],
@@ -1217,9 +1217,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
- "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz",
+ "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==",
"cpu": [
"x64"
],
@@ -1231,9 +1231,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
- "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz",
+ "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==",
"cpu": [
"arm64"
],
@@ -1245,9 +1245,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
- "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz",
+ "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==",
"cpu": [
"arm64"
],
@@ -1259,9 +1259,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
- "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz",
+ "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==",
"cpu": [
"ia32"
],
@@ -1273,9 +1273,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
- "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz",
+ "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==",
"cpu": [
"x64"
],
@@ -1287,9 +1287,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
- "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz",
+ "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==",
"cpu": [
"x64"
],
@@ -1317,9 +1317,9 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.97.0",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.97.0.tgz",
- "integrity": "sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==",
+ "version": "5.100.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.6.tgz",
+ "integrity": "sha512-Os2CPUr98to98RYm+D4qGqGkiffn7MGSyl2547a4MljVkHE30AMJRqTiyCqBfMwzAx/I91vCkAxp5tHSla6Twg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -1327,13 +1327,13 @@
}
},
"node_modules/@tanstack/vue-query": {
- "version": "5.97.0",
- "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.97.0.tgz",
- "integrity": "sha512-lziGgJTrEVXX98nQ9D+MfvjHgD92uHPrVXjB1GSfK+mq0rTr/I0ovwraGqaNTfwORmzHtQOObksPVf2NH17djw==",
+ "version": "5.100.6",
+ "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.100.6.tgz",
+ "integrity": "sha512-aUvBla5roKKI311C4KHg+n0dHe+s9MKXwhUcro6Ajvu0L6IERoZ+/CjRqUCUgd5Y51ytuZAJl85uahgp7ViVlg==",
"license": "MIT",
"dependencies": {
"@tanstack/match-sorter-utils": "^8.19.4",
- "@tanstack/query-core": "5.97.0",
+ "@tanstack/query-core": "5.100.6",
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
@@ -1394,53 +1394,53 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
- "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz",
+ "integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.2",
- "@vue/shared": "3.5.32",
+ "@vue/shared": "3.5.33",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
- "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz",
+ "integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-core": "3.5.33",
+ "@vue/shared": "3.5.33"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
- "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz",
+ "integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.2",
- "@vue/compiler-core": "3.5.32",
- "@vue/compiler-dom": "3.5.32",
- "@vue/compiler-ssr": "3.5.32",
- "@vue/shared": "3.5.32",
+ "@vue/compiler-core": "3.5.33",
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/compiler-ssr": "3.5.33",
+ "@vue/shared": "3.5.33",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
- "postcss": "^8.5.8",
+ "postcss": "^8.5.10",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
- "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz",
+ "integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/shared": "3.5.33"
}
},
"node_modules/@vue/devtools-api": {
@@ -1450,53 +1450,53 @@
"license": "MIT"
},
"node_modules/@vue/reactivity": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
- "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz",
+ "integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==",
"license": "MIT",
"dependencies": {
- "@vue/shared": "3.5.32"
+ "@vue/shared": "3.5.33"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
- "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz",
+ "integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/reactivity": "3.5.33",
+ "@vue/shared": "3.5.33"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
- "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz",
+ "integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.32",
- "@vue/runtime-core": "3.5.32",
- "@vue/shared": "3.5.32",
+ "@vue/reactivity": "3.5.33",
+ "@vue/runtime-core": "3.5.33",
+ "@vue/shared": "3.5.33",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
- "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz",
+ "integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-ssr": "3.5.33",
+ "@vue/shared": "3.5.33"
},
"peerDependencies": {
- "vue": "3.5.32"
+ "vue": "3.5.33"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
- "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz",
+ "integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==",
"license": "MIT"
},
"node_modules/@vueuse/core": {
@@ -1608,9 +1608,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
- "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
+ "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1715,9 +1715,9 @@
}
},
"node_modules/element-plus": {
- "version": "2.13.6",
- "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.6.tgz",
- "integrity": "sha512-XHgwXr8Fjz6i+6BaqFhAbae/dJbG7bBAAlHrY3pWL7dpj+JcqcOyKYt4Oy5KP86FQwS1k4uIZDjCx2FyUR5lDg==",
+ "version": "2.13.7",
+ "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.13.7.tgz",
+ "integrity": "sha512-XdHATFZOyzVFL1DaHQ90IOJQSg9UnSAV+bhDW+YB5UoZ0Hxs50mwqjqfwXkuwpSag+VXXizVcErBR6Movo5daw==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^4.2.0",
@@ -1752,6 +1752,16 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
@@ -1924,9 +1934,9 @@
"optional": true
},
"node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2042,9 +2052,9 @@
}
},
"node_modules/jsonfile": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz",
- "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==",
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
"license": "MIT",
"dependencies": {
"universalify": "^2.0.0"
@@ -2092,15 +2102,15 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
"license": "MIT"
},
"node_modules/lodash-es": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
- "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
"license": "MIT"
},
"node_modules/lodash-unified": {
@@ -2323,9 +2333,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.9",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
- "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
+ "version": "8.5.12",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
+ "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
"funding": [
{
"type": "opencollective",
@@ -2351,9 +2361,9 @@
}
},
"node_modules/prettier": {
- "version": "3.8.1",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
- "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "version": "3.8.3",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz",
+ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -2445,12 +2455,13 @@
"license": "MIT"
},
"node_modules/resolve": {
- "version": "1.22.11",
- "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
- "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "version": "1.22.12",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
+ "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
"dev": true,
"license": "MIT",
"dependencies": {
+ "es-errors": "^1.3.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
@@ -2477,9 +2488,9 @@
}
},
"node_modules/rollup": {
- "version": "4.60.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
- "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
+ "version": "4.60.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz",
+ "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2493,31 +2504,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.1",
- "@rollup/rollup-android-arm64": "4.60.1",
- "@rollup/rollup-darwin-arm64": "4.60.1",
- "@rollup/rollup-darwin-x64": "4.60.1",
- "@rollup/rollup-freebsd-arm64": "4.60.1",
- "@rollup/rollup-freebsd-x64": "4.60.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
- "@rollup/rollup-linux-arm64-gnu": "4.60.1",
- "@rollup/rollup-linux-arm64-musl": "4.60.1",
- "@rollup/rollup-linux-loong64-gnu": "4.60.1",
- "@rollup/rollup-linux-loong64-musl": "4.60.1",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
- "@rollup/rollup-linux-ppc64-musl": "4.60.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
- "@rollup/rollup-linux-riscv64-musl": "4.60.1",
- "@rollup/rollup-linux-s390x-gnu": "4.60.1",
- "@rollup/rollup-linux-x64-gnu": "4.60.1",
- "@rollup/rollup-linux-x64-musl": "4.60.1",
- "@rollup/rollup-openbsd-x64": "4.60.1",
- "@rollup/rollup-openharmony-arm64": "4.60.1",
- "@rollup/rollup-win32-arm64-msvc": "4.60.1",
- "@rollup/rollup-win32-ia32-msvc": "4.60.1",
- "@rollup/rollup-win32-x64-gnu": "4.60.1",
- "@rollup/rollup-win32-x64-msvc": "4.60.1",
+ "@rollup/rollup-android-arm-eabi": "4.60.2",
+ "@rollup/rollup-android-arm64": "4.60.2",
+ "@rollup/rollup-darwin-arm64": "4.60.2",
+ "@rollup/rollup-darwin-x64": "4.60.2",
+ "@rollup/rollup-freebsd-arm64": "4.60.2",
+ "@rollup/rollup-freebsd-x64": "4.60.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.2",
+ "@rollup/rollup-linux-arm64-musl": "4.60.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.2",
+ "@rollup/rollup-linux-loong64-musl": "4.60.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-gnu": "4.60.2",
+ "@rollup/rollup-linux-x64-musl": "4.60.2",
+ "@rollup/rollup-openbsd-x64": "4.60.2",
+ "@rollup/rollup-openharmony-arm64": "4.60.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.2",
+ "@rollup/rollup-win32-x64-gnu": "4.60.2",
+ "@rollup/rollup-win32-x64-msvc": "4.60.2",
"fsevents": "~2.3.2"
}
},
@@ -2742,14 +2753,14 @@
}
},
"node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz",
- "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz",
+ "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "confbox": "^0.2.2",
- "exsolve": "^1.0.7",
+ "confbox": "^0.2.4",
+ "exsolve": "^1.0.8",
"pathe": "^2.0.3"
}
},
@@ -2995,16 +3006,16 @@
}
},
"node_modules/vue": {
- "version": "3.5.32",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
- "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
+ "version": "3.5.33",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz",
+ "integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.32",
- "@vue/compiler-sfc": "3.5.32",
- "@vue/runtime-dom": "3.5.32",
- "@vue/server-renderer": "3.5.32",
- "@vue/shared": "3.5.32"
+ "@vue/compiler-dom": "3.5.33",
+ "@vue/compiler-sfc": "3.5.33",
+ "@vue/runtime-dom": "3.5.33",
+ "@vue/server-renderer": "3.5.33",
+ "@vue/shared": "3.5.33"
},
"peerDependencies": {
"typescript": "*"
@@ -3016,9 +3027,9 @@
}
},
"node_modules/vue-component-type-helpers": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.6.tgz",
- "integrity": "sha512-O02tnvIfOQVmnvoWwuSydwRoHjZVt8UEBR+2p4rT35p8GAy5VTlWP8o5qXfJR/GWCN0nVZoYWsVUvx2jwgdBmQ==",
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-3.2.7.tgz",
+ "integrity": "sha512-+gPp5YGmhfsj1IN+xUo7y0fb4clfnOiiUA39y07yW1VzCRjzVgwLbtmdWlghh7mXrPsEaYc7rrIir/HT6C8vYQ==",
"license": "MIT"
},
"node_modules/vue-demi": {
(skywalking-banyandb) 12/24: Fix lifecycle migration failure when the target stage has `close: true` (#1077)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit d1ee661b33700ebe5bf47e6c2d8088b0511b270c
Author: mrproliu <74...@qq.com>
AuthorDate: Mon Apr 20 08:47:32 2026 +0800
Fix lifecycle migration failure when the target stage has `close: true` (#1077)
* Fix lifecycle migration failure when the target stage has `close: true`
---
banyand/measure/write_data.go | 14 +-
banyand/measure/write_data_segmentref_test.go | 250 ++++++++++++++++++
banyand/stream/write_data.go | 14 +-
banyand/stream/write_data_segmentref_test.go | 248 ++++++++++++++++++
banyand/trace/write_data.go | 22 +-
banyand/trace/write_data_segmentref_test.go | 354 ++++++++++++++++++++++++++
6 files changed, 895 insertions(+), 7 deletions(-)
diff --git a/banyand/measure/write_data.go b/banyand/measure/write_data.go
index 2f848f111..2ecf535bf 100644
--- a/banyand/measure/write_data.go
+++ b/banyand/measure/write_data.go
@@ -34,7 +34,12 @@ import (
)
type syncPartContext struct {
- tsTable *tsTable
+ tsTable *tsTable
+ // segment is held until Close. On stages with Close=true, closeIdleSegments
+ // can drive refCount to 0; releasing earlier lets each sync call bottom out
+ // there, so the next session's incRef re-runs initTSTable and deletes the
+ // in-flight part.
+ segment storage.Segment[*tsTable, *commonv1.ResourceOpts]
fileSystem fs.FileSystem
writers *writers
partPath string
@@ -76,6 +81,10 @@ func (s *syncPartContext) Close() error {
s.fileSystem.MustRMAll(s.partPath)
s.partPath = ""
}
+ if s.segment != nil {
+ s.segment.DecRef()
+ s.segment = nil
+ }
s.tsTable = nil
s.fileSystem = nil
return nil
@@ -113,9 +122,9 @@ func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (que
s.l.Error().Err(err).Str("group", ctx.Group).Time("segmentTime", segmentTime).Msg("failed to create segment")
return nil, err
}
- defer segment.DecRef()
tsTable, err := segment.CreateTSTableIfNotExist(common.ShardID(ctx.ShardID))
if err != nil {
+ segment.DecRef()
s.l.Error().Err(err).Str("group", ctx.Group).Uint32("shardID", ctx.ShardID).Msg("failed to create ts table")
return nil, err
}
@@ -131,6 +140,7 @@ func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (que
partCtx := &syncPartContext{
tsTable: tsTable,
+ segment: segment,
fileSystem: fileSystem,
writers: w,
partPath: pp,
diff --git a/banyand/measure/write_data_segmentref_test.go b/banyand/measure/write_data_segmentref_test.go
new file mode 100644
index 000000000..0606cc204
--- /dev/null
+++ b/banyand/measure/write_data_segmentref_test.go
@@ -0,0 +1,250 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 measure
+
+import (
+ "context"
+ "io"
+ "os"
+ "path/filepath"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
+ metadataschema "github.com/apache/skywalking-banyandb/banyand/metadata/schema"
+ "github.com/apache/skywalking-banyandb/banyand/protector"
+ "github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ resourceSchema "github.com/apache/skywalking-banyandb/pkg/schema"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/timestamp"
+)
+
+// TestSyncReceiver_SegmentRefOwnership is the regression test for the
+// cold-tier lifecycle sync crash (measure equivalent of the trace test of the
+// same name). See the trace package for the full design rationale. Summary:
+// syncCallback.CreatePartHandler must hold the segment reference for the
+// entire sync session so that initTSTable does not re-run mid-stream and
+// delete the partial part directory.
+func TestSyncReceiver_SegmentRefOwnership(t *testing.T) {
+ tmpPath, cleanup := test.Space(require.New(t))
+ defer cleanup()
+
+ var openShardCount atomic.Int32
+ db := openTestTSDBForRefTest(t, tmpPath, &openShardCount)
+ defer db.Close()
+
+ segTime := time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC)
+
+ seg, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ seg.DecRef()
+ seg.DecRef() // refCount -> 0 (idle-closed)
+ baselineOpens := openShardCount.Load()
+
+ segA, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ tsA, err := segA.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ tsA.curPartID++
+ partPathA := partPath(tsA.root, tsA.curPartID)
+ require.NoError(t, os.MkdirAll(partPathA, storage.DirPerm))
+ partCtxA := &syncPartContext{
+ tsTable: tsA,
+ segment: segA,
+ partID: tsA.curPartID,
+ }
+
+ opensAfterA := openShardCount.Load()
+ require.Equal(t, int32(1), opensAfterA-baselineOpens,
+ "Part A should re-initialize the idle-closed segment once")
+
+ segB, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ _, err = segB.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ partCtxB := &syncPartContext{
+ tsTable: tsA,
+ segment: segB,
+ }
+
+ require.Equal(t, opensAfterA, openShardCount.Load(),
+ "REGRESSION: Part B re-opened the shard -- refCount dropped to 0 between A and B")
+ _, err = os.Stat(partPathA)
+ require.NoError(t, err,
+ "REGRESSION: partA's in-flight directory was deleted while another sync session started")
+
+ require.NoError(t, partCtxA.Close())
+ require.Nil(t, partCtxA.segment, "REGRESSION: Close must clear syncPartContext.segment")
+ require.NoError(t, partCtxB.Close())
+ require.Nil(t, partCtxB.segment, "REGRESSION: Close must clear syncPartContext.segment")
+
+ beforeProbe := openShardCount.Load()
+ segC, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ defer segC.DecRef()
+ _, err = segC.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ require.Equal(t, int32(1), openShardCount.Load()-beforeProbe,
+ "REGRESSION: Close did not call segment.DecRef()")
+}
+
+// TestSyncChunkCallback_CreatePartHandler_StoresSegment drives the real
+// syncCallback.CreatePartHandler through a minimal schemaRepo shim and
+// asserts that segment ownership transfers to partCtx and that Close
+// actually DecRefs. See trace package for rationale.
+func TestSyncChunkCallback_CreatePartHandler_StoresSegment(t *testing.T) {
+ tmpPath, cleanup := test.Space(require.New(t))
+ defer cleanup()
+
+ var openShardCount atomic.Int32
+ db := openTestTSDBForRefTest(t, tmpPath, &openShardCount)
+ defer db.Close()
+
+ segTime := time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC)
+
+ warmup, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ warmup.DecRef()
+ warmup.DecRef()
+ baselineOpens := openShardCount.Load()
+
+ const groupName = "test-group"
+ callback := &syncCallback{
+ l: logger.GetLogger("test"),
+ schemaRepo: newTestSchemaRepo(db, groupName),
+ }
+
+ ctx := &queue.ChunkedSyncPartContext{
+ ID: 1,
+ Group: groupName,
+ ShardID: 0,
+ MinTimestamp: segTime.UnixNano(),
+ MaxTimestamp: segTime.UnixNano(),
+ }
+
+ handler, err := callback.CreatePartHandler(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, handler)
+
+ partCtx, ok := handler.(*syncPartContext)
+ require.True(t, ok, "CreatePartHandler must return *syncPartContext")
+ require.NotNil(t, partCtx.segment,
+ "REGRESSION: CreatePartHandler did not store segment in syncPartContext")
+
+ afterCreate := openShardCount.Load()
+ require.Equal(t, int32(1), afterCreate-baselineOpens,
+ "CreatePartHandler should have re-initialized the idle-closed segment once")
+
+ // Mid-stream probe: under fix, refCount>=1 held by partCtx -> AddInt32 (no init).
+ // Under bug (defer DecRef re-introduced), refCount=0 by now -> initialize fires.
+ segMid, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ require.Equal(t, afterCreate, openShardCount.Load(),
+ "REGRESSION: a second CreateSegmentIfNotExist between CreatePartHandler and "+
+ "Close re-initialized -- `defer segment.DecRef()` has been re-introduced.")
+ segMid.DecRef()
+
+ require.NoError(t, partCtx.Close())
+ require.Nil(t, partCtx.segment,
+ "REGRESSION: Close must clear syncPartContext.segment")
+
+ beforeProbe := openShardCount.Load()
+ segProbe, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ defer segProbe.DecRef()
+ _, err = segProbe.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ require.Equal(t, int32(1), openShardCount.Load()-beforeProbe,
+ "REGRESSION: Close did not call segment.DecRef()")
+}
+
+func openTestTSDBForRefTest(t *testing.T, tmpPath string, openShardCount *atomic.Int32) storage.TSDB[*tsTable, option] {
+ t.Helper()
+ ir := storage.IntervalRule{Unit: storage.DAY, Num: 1}
+ opts := storage.TSDBOpts[*tsTable, option]{
+ ShardNum: 1,
+ Location: filepath.Join(tmpPath, "tab"),
+ TSTableCreator: func(fileSystem fs.FileSystem, root string, p common.Position,
+ l *logger.Logger, tr timestamp.TimeRange, opt option, m any,
+ ) (*tsTable, error) {
+ if openShardCount != nil {
+ openShardCount.Add(1)
+ }
+ return newTSTable(fileSystem, root, p, l, tr, opt, m)
+ },
+ SegmentInterval: ir,
+ TTL: ir,
+ Option: option{protector: protector.Nop{}, mergePolicy: newDefaultMergePolicyForTesting()},
+ }
+ require.NoError(t, os.MkdirAll(opts.Location, storage.DirPerm))
+ ctx := common.SetPosition(
+ context.WithValue(context.Background(), logger.ContextKey, logger.GetLogger("test")),
+ func(p common.Position) common.Position {
+ p.Database = "test"
+ return p
+ },
+ )
+ db, err := storage.OpenTSDB[*tsTable, option](ctx, opts, nil, "test-group")
+ require.NoError(t, err)
+ return db
+}
+
+func newTestSchemaRepo(db storage.TSDB[*tsTable, option], groupName string) *schemaRepo {
+ return &schemaRepo{
+ Repository: &fakeRepository{
+ groups: map[string]resourceSchema.Group{
+ groupName: &fakeGroup{tsdb: db},
+ },
+ },
+ l: logger.GetLogger("test"),
+ path: "",
+ }
+}
+
+type fakeRepository struct {
+ groups map[string]resourceSchema.Group
+}
+
+func (f *fakeRepository) Watcher() {}
+func (f *fakeRepository) Init(_ metadataschema.Kind) ([]string, []int64) { return nil, nil }
+func (f *fakeRepository) SendMetadataEvent(_ resourceSchema.MetadataEvent) {}
+func (f *fakeRepository) LoadGroup(name string) (resourceSchema.Group, bool) {
+ g, ok := f.groups[name]
+ return g, ok
+}
+func (f *fakeRepository) LoadAllGroups() []resourceSchema.Group { return nil }
+func (f *fakeRepository) LoadResource(_ *commonv1.Metadata) (resourceSchema.Resource, bool) {
+ return nil, false
+}
+func (f *fakeRepository) Close() {}
+func (f *fakeRepository) StopCh() <-chan struct{} { return nil }
+func (f *fakeRepository) DropGroup(_ string) error { return nil }
+
+type fakeGroup struct {
+ tsdb storage.TSDB[*tsTable, option]
+}
+
+func (f *fakeGroup) GetSchema() *commonv1.Group { return nil }
+func (f *fakeGroup) SupplyTSDB() io.Closer { return f.tsdb }
diff --git a/banyand/stream/write_data.go b/banyand/stream/write_data.go
index 77a0b07c5..6a6ff24da 100644
--- a/banyand/stream/write_data.go
+++ b/banyand/stream/write_data.go
@@ -33,7 +33,12 @@ import (
)
type syncPartContext struct {
- tsTable *tsTable
+ tsTable *tsTable
+ // segment is held until Close. On stages with Close=true, closeIdleSegments
+ // can drive refCount to 0; releasing earlier lets each sync call bottom out
+ // there, so the next session's incRef re-runs initTSTable and deletes the
+ // in-flight part.
+ segment storage.Segment[*tsTable, *commonv1.ResourceOpts]
fileSystem fs.FileSystem
writers *writers
partPath string
@@ -75,6 +80,10 @@ func (s *syncPartContext) Close() error {
s.fileSystem.MustRMAll(s.partPath)
s.partPath = ""
}
+ if s.segment != nil {
+ s.segment.DecRef()
+ s.segment = nil
+ }
s.tsTable = nil
s.fileSystem = nil
return nil
@@ -112,9 +121,9 @@ func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (que
s.l.Error().Err(err).Str("group", ctx.Group).Time("segmentTime", segmentTime).Msg("failed to create segment")
return nil, err
}
- defer segment.DecRef()
tsTable, err := segment.CreateTSTableIfNotExist(common.ShardID(ctx.ShardID))
if err != nil {
+ segment.DecRef()
s.l.Error().Err(err).Str("group", ctx.Group).Uint32("shardID", ctx.ShardID).Msg("failed to create ts table")
return nil, err
}
@@ -130,6 +139,7 @@ func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (que
partCtx := &syncPartContext{
tsTable: tsTable,
+ segment: segment,
fileSystem: fileSystem,
writers: w,
partPath: pp,
diff --git a/banyand/stream/write_data_segmentref_test.go b/banyand/stream/write_data_segmentref_test.go
new file mode 100644
index 000000000..81784ac1c
--- /dev/null
+++ b/banyand/stream/write_data_segmentref_test.go
@@ -0,0 +1,248 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 stream
+
+import (
+ "context"
+ "io"
+ "os"
+ "path/filepath"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
+ metadataschema "github.com/apache/skywalking-banyandb/banyand/metadata/schema"
+ "github.com/apache/skywalking-banyandb/banyand/protector"
+ "github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ resourceSchema "github.com/apache/skywalking-banyandb/pkg/schema"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/timestamp"
+)
+
+// TestSyncReceiver_SegmentRefOwnership is the regression test for the
+// cold-tier lifecycle sync crash (stream equivalent of the trace test of the
+// same name). See the trace package for the full design rationale. Summary:
+// syncCallback.CreatePartHandler must hold the segment reference for the
+// entire sync session so that initTSTable does not re-run mid-stream and
+// delete the partial part directory.
+func TestSyncReceiver_SegmentRefOwnership(t *testing.T) {
+ tmpPath, cleanup := test.Space(require.New(t))
+ defer cleanup()
+
+ var openShardCount atomic.Int32
+ db := openTestTSDBForRefTest(t, tmpPath, &openShardCount)
+ defer db.Close()
+
+ segTime := time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC)
+
+ seg, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ seg.DecRef()
+ seg.DecRef() // refCount -> 0 (idle-closed)
+ baselineOpens := openShardCount.Load()
+
+ segA, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ tsA, err := segA.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ tsA.curPartID++
+ partPathA := partPath(tsA.root, tsA.curPartID)
+ require.NoError(t, os.MkdirAll(partPathA, storage.DirPerm))
+ partCtxA := &syncPartContext{
+ tsTable: tsA,
+ segment: segA,
+ partID: tsA.curPartID,
+ }
+
+ opensAfterA := openShardCount.Load()
+ require.Equal(t, int32(1), opensAfterA-baselineOpens,
+ "Part A should re-initialize the idle-closed segment once")
+
+ segB, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ _, err = segB.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ partCtxB := &syncPartContext{
+ tsTable: tsA,
+ segment: segB,
+ }
+
+ require.Equal(t, opensAfterA, openShardCount.Load(),
+ "REGRESSION: Part B re-opened the shard -- refCount dropped to 0 between A and B")
+ _, err = os.Stat(partPathA)
+ require.NoError(t, err,
+ "REGRESSION: partA's in-flight directory was deleted while another sync session started")
+
+ require.NoError(t, partCtxA.Close())
+ require.Nil(t, partCtxA.segment, "REGRESSION: Close must clear syncPartContext.segment")
+ require.NoError(t, partCtxB.Close())
+ require.Nil(t, partCtxB.segment, "REGRESSION: Close must clear syncPartContext.segment")
+
+ beforeProbe := openShardCount.Load()
+ segC, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ defer segC.DecRef()
+ _, err = segC.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ require.Equal(t, int32(1), openShardCount.Load()-beforeProbe,
+ "REGRESSION: Close did not call segment.DecRef()")
+}
+
+// TestSyncChunkCallback_CreatePartHandler_StoresSegment drives the real
+// syncCallback.CreatePartHandler through a minimal schemaRepo shim and
+// asserts that segment ownership transfers to partCtx and that Close
+// actually DecRefs. See trace package for rationale.
+func TestSyncChunkCallback_CreatePartHandler_StoresSegment(t *testing.T) {
+ tmpPath, cleanup := test.Space(require.New(t))
+ defer cleanup()
+
+ var openShardCount atomic.Int32
+ db := openTestTSDBForRefTest(t, tmpPath, &openShardCount)
+ defer db.Close()
+
+ segTime := time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC)
+
+ warmup, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ warmup.DecRef()
+ warmup.DecRef()
+ baselineOpens := openShardCount.Load()
+
+ const groupName = "test-group"
+ callback := &syncCallback{
+ l: logger.GetLogger("test"),
+ schemaRepo: newTestSchemaRepo(db, groupName),
+ }
+
+ ctx := &queue.ChunkedSyncPartContext{
+ ID: 1,
+ Group: groupName,
+ ShardID: 0,
+ MinTimestamp: segTime.UnixNano(),
+ MaxTimestamp: segTime.UnixNano(),
+ }
+
+ handler, err := callback.CreatePartHandler(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, handler)
+
+ partCtx, ok := handler.(*syncPartContext)
+ require.True(t, ok, "CreatePartHandler must return *syncPartContext")
+ require.NotNil(t, partCtx.segment,
+ "REGRESSION: CreatePartHandler did not store segment in syncPartContext")
+
+ afterCreate := openShardCount.Load()
+ require.Equal(t, int32(1), afterCreate-baselineOpens,
+ "CreatePartHandler should have re-initialized the idle-closed segment once")
+
+ segMid, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ require.Equal(t, afterCreate, openShardCount.Load(),
+ "REGRESSION: a second CreateSegmentIfNotExist between CreatePartHandler and "+
+ "Close re-initialized -- `defer segment.DecRef()` has been re-introduced.")
+ segMid.DecRef()
+
+ require.NoError(t, partCtx.Close())
+ require.Nil(t, partCtx.segment,
+ "REGRESSION: Close must clear syncPartContext.segment")
+
+ beforeProbe := openShardCount.Load()
+ segProbe, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ defer segProbe.DecRef()
+ _, err = segProbe.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ require.Equal(t, int32(1), openShardCount.Load()-beforeProbe,
+ "REGRESSION: Close did not call segment.DecRef()")
+}
+
+func openTestTSDBForRefTest(t *testing.T, tmpPath string, openShardCount *atomic.Int32) storage.TSDB[*tsTable, option] {
+ t.Helper()
+ ir := storage.IntervalRule{Unit: storage.DAY, Num: 1}
+ opts := storage.TSDBOpts[*tsTable, option]{
+ ShardNum: 1,
+ Location: filepath.Join(tmpPath, "tab"),
+ TSTableCreator: func(fileSystem fs.FileSystem, root string, p common.Position,
+ l *logger.Logger, tr timestamp.TimeRange, opt option, m any,
+ ) (*tsTable, error) {
+ if openShardCount != nil {
+ openShardCount.Add(1)
+ }
+ return newTSTable(fileSystem, root, p, l, tr, opt, m)
+ },
+ SegmentInterval: ir,
+ TTL: ir,
+ Option: option{protector: protector.Nop{}, mergePolicy: newDefaultMergePolicyForTesting()},
+ }
+ require.NoError(t, os.MkdirAll(opts.Location, storage.DirPerm))
+ ctx := common.SetPosition(
+ context.WithValue(context.Background(), logger.ContextKey, logger.GetLogger("test")),
+ func(p common.Position) common.Position {
+ p.Database = "test"
+ return p
+ },
+ )
+ db, err := storage.OpenTSDB[*tsTable, option](ctx, opts, nil, "test-group")
+ require.NoError(t, err)
+ return db
+}
+
+func newTestSchemaRepo(db storage.TSDB[*tsTable, option], groupName string) *schemaRepo {
+ return &schemaRepo{
+ Repository: &fakeRepository{
+ groups: map[string]resourceSchema.Group{
+ groupName: &fakeGroup{tsdb: db},
+ },
+ },
+ l: logger.GetLogger("test"),
+ path: "",
+ }
+}
+
+type fakeRepository struct {
+ groups map[string]resourceSchema.Group
+}
+
+func (f *fakeRepository) Watcher() {}
+func (f *fakeRepository) Init(_ metadataschema.Kind) ([]string, []int64) { return nil, nil }
+func (f *fakeRepository) SendMetadataEvent(_ resourceSchema.MetadataEvent) {}
+func (f *fakeRepository) LoadGroup(name string) (resourceSchema.Group, bool) {
+ g, ok := f.groups[name]
+ return g, ok
+}
+func (f *fakeRepository) LoadAllGroups() []resourceSchema.Group { return nil }
+func (f *fakeRepository) LoadResource(_ *commonv1.Metadata) (resourceSchema.Resource, bool) {
+ return nil, false
+}
+func (f *fakeRepository) Close() {}
+func (f *fakeRepository) StopCh() <-chan struct{} { return nil }
+func (f *fakeRepository) DropGroup(_ string) error { return nil }
+
+type fakeGroup struct {
+ tsdb storage.TSDB[*tsTable, option]
+}
+
+func (f *fakeGroup) GetSchema() *commonv1.Group { return nil }
+func (f *fakeGroup) SupplyTSDB() io.Closer { return f.tsdb }
diff --git a/banyand/trace/write_data.go b/banyand/trace/write_data.go
index 5c6e6d302..0d339d71f 100644
--- a/banyand/trace/write_data.go
+++ b/banyand/trace/write_data.go
@@ -35,7 +35,12 @@ import (
)
type syncPartContext struct {
- tsTable *tsTable
+ tsTable *tsTable
+ // segment is held until Close. On stages with Close=true, closeIdleSegments
+ // can drive refCount to 0; releasing earlier lets each sync call bottom out
+ // there, so the next session's incRef re-runs initTSTable and deletes the
+ // in-flight part.
+ segment storage.Segment[*tsTable, *commonv1.ResourceOpts]
fileSystem fs.FileSystem
l *logger.Logger
writers *writers
@@ -140,6 +145,10 @@ func (s *syncPartContext) Close() error {
}
s.sidxPartContexts = nil
}
+ if s.segment != nil {
+ s.segment.DecRef()
+ s.segment = nil
+ }
s.tsTable = nil
s.fileSystem = nil
s.traceIDFilterBuffer = nil
@@ -284,9 +293,9 @@ func (s *syncChunkCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext)
s.l.Error().Err(err).Str("group", ctx.Group).Time("segmentTime", segmentTime).Msg("failed to create segment")
return nil, err
}
- defer segment.DecRef()
tsTable, err := segment.CreateTSTableIfNotExist(common.ShardID(ctx.ShardID))
if err != nil {
+ segment.DecRef()
s.l.Error().Err(err).Str("group", ctx.Group).Uint32("shardID", ctx.ShardID).Msg("failed to create ts table")
return nil, err
}
@@ -294,9 +303,16 @@ func (s *syncChunkCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext)
tsdb.Tick(ctx.MaxTimestamp)
partCtx := &syncPartContext{
tsTable: tsTable,
+ segment: segment,
l: s.l,
}
- return partCtx, partCtx.NewPartType(ctx)
+ if err := partCtx.NewPartType(ctx); err != nil {
+ if closeErr := partCtx.Close(); closeErr != nil {
+ return nil, fmt.Errorf("new part type: %w; close part context: %w", err, closeErr)
+ }
+ return nil, err
+ }
+ return partCtx, nil
}
// HandleFileChunk implements queue.ChunkedSyncHandler for streaming file chunks.
diff --git a/banyand/trace/write_data_segmentref_test.go b/banyand/trace/write_data_segmentref_test.go
new file mode 100644
index 000000000..f332141d8
--- /dev/null
+++ b/banyand/trace/write_data_segmentref_test.go
@@ -0,0 +1,354 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 trace
+
+import (
+ "context"
+ "io"
+ "os"
+ "path/filepath"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/common"
+ commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
+ metadataschema "github.com/apache/skywalking-banyandb/banyand/metadata/schema"
+ "github.com/apache/skywalking-banyandb/banyand/protector"
+ "github.com/apache/skywalking-banyandb/banyand/queue"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ resourceSchema "github.com/apache/skywalking-banyandb/pkg/schema"
+ "github.com/apache/skywalking-banyandb/pkg/test"
+ "github.com/apache/skywalking-banyandb/pkg/timestamp"
+)
+
+// TestSyncReceiver_SegmentRefOwnership is the regression test for the
+// cold-tier lifecycle sync crash. It drives the real tsdb / segment
+// machinery and the real syncPartContext.Close method, and asserts both the
+// refcount-level invariant (no mid-stream re-initialization) and the
+// filesystem-level invariant (the in-flight part directory survives while
+// another sync session starts).
+//
+// The bug: syncChunkCallback.CreatePartHandler released its segment reference
+// via `defer segment.DecRef()`, which drops refCount to 0 on an idle-closed
+// segment. The next CreatePartHandler's incRef hits the `initialize` branch,
+// which re-runs loadShards -> initTSTable and deletes any in-progress part
+// (no metadata.json yet). Fix: hold the segment in syncPartContext.segment and
+// DecRef only in Close / FinishSync, matching write_standalone.go's pattern.
+//
+// What this test catches (any of these regressions makes it fail):
+// 1. syncPartContext struct loses the `segment` field -> compile error
+// 2. CreatePartHandler stops storing segment in partCtx -> on-disk partA dir gets deleted
+// 3. Close stops calling s.segment.DecRef() -> final probe assertion fails
+// 4. Close stops nil'ing s.segment -> require.Nil(partCtxA.segment) fails
+// 5. defer segment.DecRef() is reintroduced in CreatePartHandler -> on-disk partA dir gets deleted
+func TestSyncReceiver_SegmentRefOwnership(t *testing.T) {
+ tmpPath, cleanup := test.Space(require.New(t))
+ defer cleanup()
+
+ // TSTableCreator hook so we can count how many times loadShards opens a
+ // shard. Each invocation means segment.refCount was 0 immediately before
+ // the triggering CreateSegmentIfNotExist call.
+ var openShardCount atomic.Int32
+ db := openTestTSDBForRefTest(t, tmpPath, &openShardCount)
+ defer db.Close()
+
+ segTime := time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC)
+
+ // Drive the segment into the cold-0 "idle-closed" state: segment object
+ // still exists in sc.lst but refCount=0 and shards have been released.
+ seg, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ seg.DecRef() // normal caller release -> refCount = 1
+ seg.DecRef() // simulate idle close -> refCount = 0
+ baselineOpens := openShardCount.Load()
+
+ // ---------- Part A: simulate syncChunkCallback.CreatePartHandler (fix) ----------
+ // Sequence matches the production CreatePartHandler under the fix:
+ // CreateSegmentIfNotExist -> CreateTSTableIfNotExist -> NewPartType (we
+ // stand in for NewPartType by creating the on-disk part directory without
+ // writing metadata.json, mirroring mid-stream state) -> store segment in
+ // syncPartContext. Do NOT DecRef here.
+ segA, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ tsA, err := segA.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ tsA.curPartID++
+ partPathA := partPath(tsA.root, tsA.curPartID)
+ require.NoError(t, os.MkdirAll(partPathA, storage.DirPerm),
+ "simulate NewPartType creating the part directory")
+ partCtxA := &syncPartContext{
+ tsTable: tsA,
+ segment: segA, // fix: store segment for the whole session
+ l: logger.GetLogger("test"),
+ }
+
+ opensAfterA := openShardCount.Load()
+ require.Equal(t, int32(1), opensAfterA-baselineOpens,
+ "Part A's CreateSegmentIfNotExist should re-initialize exactly once "+
+ "(segment was idle-closed, so refCount=0 -> initialize branch)")
+
+ // ---------- Part B: another sync session starts while A is still in flight ----------
+ // With the fix, segA keeps refCount>=1, so Part B's CreateSegmentIfNotExist
+ // takes the AddInt32 branch -- no initialize, no loadShards, no initTSTable
+ // rescan -> partPathA is not touched.
+ segB, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ _, err = segB.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+ partCtxB := &syncPartContext{
+ tsTable: tsA,
+ segment: segB,
+ l: logger.GetLogger("test"),
+ }
+
+ // Refcount-level invariant: no re-initialization between A and B.
+ opensAfterB := openShardCount.Load()
+ require.Equal(t, opensAfterA, opensAfterB,
+ "REGRESSION: Part B re-opened the shard, meaning segment.refCount dropped to 0 "+
+ "between Part A and Part B. Check that syncPartContext stores segment and "+
+ "that CreatePartHandler no longer runs `defer segment.DecRef()`.")
+
+ // Filesystem-level invariant: partA's in-flight directory survives.
+ // If initTSTable re-ran (it would, under the bug), it would have taken the
+ // "cannot validate part metadata. skip and delete it" branch because
+ // partPathA has no metadata.json yet.
+ _, err = os.Stat(partPathA)
+ require.NoError(t, err,
+ "REGRESSION: partA's in-flight directory was deleted while another sync "+
+ "session started. initTSTable must not re-run mid-stream.")
+
+ // ---------- Exercise syncPartContext.Close (fix path) ----------
+ require.NoError(t, partCtxA.Close())
+ require.Nil(t, partCtxA.segment,
+ "REGRESSION: Close must clear syncPartContext.segment (`s.segment = nil`)")
+
+ require.NoError(t, partCtxB.Close())
+ require.Nil(t, partCtxB.segment,
+ "REGRESSION: Close must clear syncPartContext.segment (`s.segment = nil`)")
+
+ // ---------- Verify Close actually DecRef'd (not just cleared the field) ----------
+ // After both Closes, refCount should be 0. The next CreateSegmentIfNotExist
+ // must therefore re-initialize and invoke TSTableCreator. If Close forgot
+ // to call s.segment.DecRef(), refCount would still be >=1 and this
+ // assertion would fail.
+ segC, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ defer segC.DecRef()
+ _, err = segC.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+
+ opensAfterC := openShardCount.Load()
+ require.Equal(t, int32(1), opensAfterC-opensAfterB,
+ "REGRESSION: after closing both partCtx, the next CreateSegmentIfNotExist did "+
+ "not re-initialize. This means Close did not call segment.DecRef(). "+
+ "Check `s.segment.DecRef()` is still in syncPartContext.Close.")
+}
+
+func openTestTSDBForRefTest(t *testing.T, tmpPath string, openShardCount *atomic.Int32) storage.TSDB[*tsTable, option] {
+ t.Helper()
+ ir := storage.IntervalRule{Unit: storage.DAY, Num: 1}
+ opts := storage.TSDBOpts[*tsTable, option]{
+ ShardNum: 1,
+ Location: filepath.Join(tmpPath, "tab"),
+ // Wrap newTSTable so we can count how many times loadShards opens a
+ // shard. Each call implies initTSTable ran, which means segment
+ // refCount was 0 before and went through the `initialize` branch of
+ // incRef. Counting these calls is how we observe whether the fix
+ // holds refCount >0 across sibling CreatePartHandler invocations.
+ TSTableCreator: func(fileSystem fs.FileSystem, root string, p common.Position,
+ l *logger.Logger, tr timestamp.TimeRange, opt option, m any,
+ ) (*tsTable, error) {
+ if openShardCount != nil {
+ openShardCount.Add(1)
+ }
+ return newTSTable(fileSystem, root, p, l, tr, opt, m)
+ },
+ SegmentInterval: ir,
+ TTL: ir,
+ Option: option{protector: protector.Nop{}, mergePolicy: newDefaultMergePolicyForTesting()},
+ }
+ require.NoError(t, os.MkdirAll(opts.Location, storage.DirPerm))
+ ctx := common.SetPosition(
+ context.WithValue(context.Background(), logger.ContextKey, logger.GetLogger("test")),
+ func(p common.Position) common.Position {
+ p.Database = "test"
+ return p
+ },
+ )
+ db, err := storage.OpenTSDB[*tsTable, option](ctx, opts, nil, "test-group")
+ require.NoError(t, err)
+ return db
+}
+
+// TestSyncChunkCallback_CreatePartHandler_StoresSegment drives the real
+// syncChunkCallback.CreatePartHandler through a minimal schemaRepo shim and
+// asserts that:
+//
+// 1. The returned PartHandler is a *syncPartContext whose `segment` field is
+// non-nil (i.e. CreatePartHandler transferred segment ownership to the
+// context — catches someone reverting `segment: segment` in the struct
+// literal or re-introducing `defer segment.DecRef()`).
+// 2. syncPartContext.Close releases the segment (field cleared AND DecRef
+// actually happened — observable via the TSTableCreator counter).
+//
+// This complements TestSyncReceiver_SegmentRefOwnership, which builds
+// syncPartContext manually. Together they cover every line of the fix.
+func TestSyncChunkCallback_CreatePartHandler_StoresSegment(t *testing.T) {
+ tmpPath, cleanup := test.Space(require.New(t))
+ defer cleanup()
+
+ var openShardCount atomic.Int32
+ db := openTestTSDBForRefTest(t, tmpPath, &openShardCount)
+ defer db.Close()
+
+ segTime := time.Date(2026, 4, 17, 0, 0, 0, 0, time.UTC)
+
+ // Drive the segment into the idle-closed state (refCount=0) so that the
+ // probe at the end of this test reliably exercises the `initialize`
+ // branch of incRef if and only if Close actually DecRef'd.
+ warmup, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ warmup.DecRef()
+ warmup.DecRef() // refCount -> 0
+ baselineOpens := openShardCount.Load()
+
+ const groupName = "test-group"
+ callback := &syncChunkCallback{
+ l: logger.GetLogger("test"),
+ schemaRepo: newTestSchemaRepo(db, groupName),
+ }
+
+ ctx := &queue.ChunkedSyncPartContext{
+ ID: 1,
+ Group: groupName,
+ ShardID: 0,
+ MinTimestamp: segTime.UnixNano(),
+ MaxTimestamp: segTime.UnixNano(),
+ PartType: PartTypeCore,
+ }
+
+ handler, err := callback.CreatePartHandler(ctx)
+ require.NoError(t, err)
+ require.NotNil(t, handler)
+
+ partCtx, ok := handler.(*syncPartContext)
+ require.True(t, ok, "CreatePartHandler must return *syncPartContext")
+ require.NotNil(t, partCtx.segment,
+ "REGRESSION: CreatePartHandler did not store segment in syncPartContext. "+
+ "Check the `segment: segment` field assignment is still present in the "+
+ "partCtx struct literal and that `defer segment.DecRef()` has not been "+
+ "re-introduced at the top of CreatePartHandler.")
+
+ // At this point CreatePartHandler has driven refCount 0 -> 1 via initialize
+ // (loadShards ran -> TSTableCreator was called once).
+ afterCreate := openShardCount.Load()
+ require.Equal(t, int32(1), afterCreate-baselineOpens,
+ "CreatePartHandler should have re-initialized the idle-closed segment once")
+
+ // CRITICAL mid-stream probe (between CreatePartHandler and Close).
+ // Under the fix, partCtx holds segment.refCount >= 1, so a concurrent
+ // CreateSegmentIfNotExist takes the AddInt32 branch and does NOT re-init.
+ // Under the bug (defer segment.DecRef() in CreatePartHandler), refCount
+ // already dropped to 0 at CreatePartHandler return, performCleanup ran,
+ // and this concurrent call re-initializes -> TSTableCreator fires again.
+ // This is the assertion that catches someone re-introducing the defer.
+ segMid, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ require.Equal(t, afterCreate, openShardCount.Load(),
+ "REGRESSION: a second CreateSegmentIfNotExist between CreatePartHandler and "+
+ "Close re-initialized the segment. That means segment.refCount dropped to 0 "+
+ "when CreatePartHandler returned -- i.e. `defer segment.DecRef()` was "+
+ "re-introduced or `segment: segment` was removed from the partCtx struct "+
+ "literal.")
+ segMid.DecRef() // release the mid-probe's own incRef
+
+ // Close. This should:
+ // - call s.segment.DecRef() -> refCount 1 -> 0
+ // - set s.segment = nil
+ // Neither is observable directly from outside the package, so we probe
+ // with a follow-up CreateSegmentIfNotExist. If refCount is back to 0 as
+ // intended, that probe will re-initialize and increment openShardCount.
+ // If Close forgot to DecRef, refCount stays at 1 and the probe takes the
+ // cheap AddInt32 path with no TSTableCreator invocation.
+ require.NoError(t, partCtx.Close())
+ require.Nil(t, partCtx.segment,
+ "REGRESSION: Close must clear syncPartContext.segment (`s.segment = nil`)")
+
+ beforeProbe := openShardCount.Load()
+ segProbe, err := db.CreateSegmentIfNotExist(segTime)
+ require.NoError(t, err)
+ defer segProbe.DecRef()
+ _, err = segProbe.CreateTSTableIfNotExist(common.ShardID(0))
+ require.NoError(t, err)
+
+ require.Equal(t, int32(1), openShardCount.Load()-beforeProbe,
+ "REGRESSION: after partCtx.Close, the next CreateSegmentIfNotExist did not "+
+ "re-initialize. This means Close did not call s.segment.DecRef(). "+
+ "Check that the Close method still contains `s.segment.DecRef()`.")
+}
+
+// newTestSchemaRepo builds a minimal *schemaRepo whose loadTSDB returns the
+// given db for the given group. All other schemaRepo methods that
+// syncChunkCallback doesn't exercise are left at their zero values.
+func newTestSchemaRepo(db storage.TSDB[*tsTable, option], groupName string) *schemaRepo {
+ return &schemaRepo{
+ Repository: &fakeRepository{
+ groups: map[string]resourceSchema.Group{
+ groupName: &fakeGroup{tsdb: db},
+ },
+ },
+ l: logger.GetLogger("test"),
+ path: "",
+ }
+}
+
+// fakeRepository is a minimal implementation of resourceSchema.Repository that
+// only supports LoadGroup. Other methods are no-ops / zero-value returns so
+// the type satisfies the interface.
+type fakeRepository struct {
+ groups map[string]resourceSchema.Group
+}
+
+func (f *fakeRepository) Watcher() {}
+func (f *fakeRepository) Init(_ metadataschema.Kind) ([]string, []int64) { return nil, nil }
+func (f *fakeRepository) SendMetadataEvent(_ resourceSchema.MetadataEvent) {}
+func (f *fakeRepository) LoadGroup(name string) (resourceSchema.Group, bool) {
+ g, ok := f.groups[name]
+ return g, ok
+}
+func (f *fakeRepository) LoadAllGroups() []resourceSchema.Group { return nil }
+func (f *fakeRepository) LoadResource(_ *commonv1.Metadata) (resourceSchema.Resource, bool) {
+ return nil, false
+}
+func (f *fakeRepository) Close() {}
+func (f *fakeRepository) StopCh() <-chan struct{} { return nil }
+func (f *fakeRepository) DropGroup(_ string) error { return nil }
+
+// fakeGroup is a minimal implementation of resourceSchema.Group that returns
+// a pre-built tsdb from SupplyTSDB.
+type fakeGroup struct {
+ tsdb storage.TSDB[*tsTable, option]
+}
+
+func (f *fakeGroup) GetSchema() *commonv1.Group { return nil }
+func (f *fakeGroup) SupplyTSDB() io.Closer { return f.tsdb }
(skywalking-banyandb) 18/24: fix(metadata): widen FODC inspection broadcast deadline and parallelize InspectAll (#1092)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit a712c2ef0e41de685138a15f041bf219c66a6bdb
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Mon Apr 27 11:31:53 2026 +0800
fix(metadata): widen FODC inspection broadcast deadline and parallelize InspectAll (#1092)
---
banyand/metadata/schema/collector.go | 10 ++++--
banyand/queue/sub/group_lifecycle.go | 35 ++++++++++++++++---
banyand/queue/sub/group_lifecycle_test.go | 57 +++++++++++++++++++++++++++++++
3 files changed, 95 insertions(+), 7 deletions(-)
diff --git a/banyand/metadata/schema/collector.go b/banyand/metadata/schema/collector.go
index d8ea0e1e0..9b297fd66 100644
--- a/banyand/metadata/schema/collector.go
+++ b/banyand/metadata/schema/collector.go
@@ -33,6 +33,12 @@ import (
"github.com/apache/skywalking-banyandb/pkg/logger"
)
+// inspectBroadcastTimeout caps how long the inspection query path waits for
+// data and liaison nodes to answer a CollectDataInfo / CollectLiaisonInfo
+// broadcast. Picked to leave headroom for busy nodes (sw_zipkinTrace under
+// merge/flush load can take several seconds) while still bounding the call.
+const inspectBroadcastTimeout = 30 * time.Second
+
// GroupGetter provides method to get group metadata.
type GroupGetter interface {
GetGroup(ctx context.Context, group string) (*commonv1.Group, error)
@@ -101,7 +107,7 @@ func (icr *InfoCollectorRegistry) CollectDataInfo(ctx context.Context, group str
func (icr *InfoCollectorRegistry) broadcastCollectDataInfo(topic bus.Topic, group string) []*databasev1.DataInfo {
message := bus.NewMessage(bus.MessageID(time.Now().UnixNano()), &databasev1.GroupRegistryServiceInspectRequest{Group: group})
- futures, broadcastErr := icr.dataBroadcaster.Broadcast(5*time.Second, topic, message)
+ futures, broadcastErr := icr.dataBroadcaster.Broadcast(inspectBroadcastTimeout, topic, message)
if broadcastErr != nil {
icr.l.Warn().Err(broadcastErr).Str("group", group).Msg("failed to broadcast collect data info request")
return []*databasev1.DataInfo{}
@@ -172,7 +178,7 @@ func (icr *InfoCollectorRegistry) CollectLiaisonInfo(ctx context.Context, group
func (icr *InfoCollectorRegistry) broadcastCollectLiaisonInfo(topic bus.Topic, group string) []*databasev1.LiaisonInfo {
message := bus.NewMessage(bus.MessageID(time.Now().UnixNano()), &databasev1.GroupRegistryServiceInspectRequest{Group: group})
- futures, broadcastErr := icr.liaisonBroadcaster.Broadcast(5*time.Second, topic, message)
+ futures, broadcastErr := icr.liaisonBroadcaster.Broadcast(inspectBroadcastTimeout, topic, message)
if broadcastErr != nil {
icr.l.Warn().Err(broadcastErr).Str("group", group).Msg("failed to broadcast collect liaison info request")
return []*databasev1.LiaisonInfo{}
diff --git a/banyand/queue/sub/group_lifecycle.go b/banyand/queue/sub/group_lifecycle.go
index 1fc8b30c0..8104015da 100644
--- a/banyand/queue/sub/group_lifecycle.go
+++ b/banyand/queue/sub/group_lifecycle.go
@@ -20,12 +20,21 @@ package sub
import (
"context"
"fmt"
+ "sync"
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
fodcv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/fodc/v1"
)
+// maxInspectGroupConcurrency caps how many groups InspectAll will inspect
+// in parallel. Picked high enough to fan out all groups in a single wave
+// for typical clusters (BanyanDB rarely has more than a few dozen groups)
+// while still bounding load on the data nodes.
+const maxInspectGroupConcurrency = 32
+
// InspectAll lists all groups and returns their full lifecycle info.
+// Per-group inspections run in parallel (bounded), so a single slow group
+// cannot consume budget that other groups need.
func (s *server) InspectAll(ctx context.Context, _ *fodcv1.InspectAllRequest) (*fodcv1.InspectAllResponse, error) {
if s.metadataRepo == nil {
return nil, fmt.Errorf("metadata repository not available")
@@ -34,15 +43,31 @@ func (s *server) InspectAll(ctx context.Context, _ *fodcv1.InspectAllRequest) (*
if err != nil {
return nil, fmt.Errorf("failed to list groups: %w", err)
}
- result := make([]*fodcv1.GroupLifecycleInfo, 0, len(groups))
- for _, group := range groups {
+ results := make([]*fodcv1.GroupLifecycleInfo, len(groups))
+ limit := max(min(len(groups), maxInspectGroupConcurrency), 1)
+ sem := make(chan struct{}, limit)
+ var wg sync.WaitGroup
+ for i, group := range groups {
if group == nil || group.Metadata == nil {
continue
}
- info := s.inspectGroup(ctx, group)
- result = append(result, info)
+ sem <- struct{}{}
+ wg.Add(1)
+ go func(idx int, g *commonv1.Group) {
+ defer wg.Done()
+ defer func() { <-sem }()
+ results[idx] = s.inspectGroup(ctx, g)
+ }(i, group)
+ }
+ wg.Wait()
+
+ out := make([]*fodcv1.GroupLifecycleInfo, 0, len(results))
+ for _, r := range results {
+ if r != nil {
+ out = append(out, r)
+ }
}
- return &fodcv1.InspectAllResponse{Groups: result}, nil
+ return &fodcv1.InspectAllResponse{Groups: out}, nil
}
func (s *server) inspectGroup(ctx context.Context, group *commonv1.Group) *fodcv1.GroupLifecycleInfo {
diff --git a/banyand/queue/sub/group_lifecycle_test.go b/banyand/queue/sub/group_lifecycle_test.go
index 2a88ef995..f3a5cd6ff 100644
--- a/banyand/queue/sub/group_lifecycle_test.go
+++ b/banyand/queue/sub/group_lifecycle_test.go
@@ -20,7 +20,9 @@ package sub
import (
"context"
"fmt"
+ "sync/atomic"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -48,6 +50,10 @@ type mockMetadataRepo struct {
groupRegistry *mockGroupRegistry
dataInfoMap map[string][]*databasev1.DataInfo
dataInfoErr map[string]error
+ slowGroups map[string]time.Duration
+ collectStarts atomic.Int32
+ concurrentMax atomic.Int32
+ concurrentNow atomic.Int32
}
func (m *mockMetadataRepo) GroupRegistry() schema.Group {
@@ -55,6 +61,20 @@ func (m *mockMetadataRepo) GroupRegistry() schema.Group {
}
func (m *mockMetadataRepo) CollectDataInfo(_ context.Context, group string) ([]*databasev1.DataInfo, error) {
+ m.collectStarts.Add(1)
+ current := m.concurrentNow.Add(1)
+ defer m.concurrentNow.Add(-1)
+ for {
+ hi := m.concurrentMax.Load()
+ if current <= hi || m.concurrentMax.CompareAndSwap(hi, current) {
+ break
+ }
+ }
+ if m.slowGroups != nil {
+ if d, ok := m.slowGroups[group]; ok && d > 0 {
+ time.Sleep(d)
+ }
+ }
if m.dataInfoErr != nil {
if err, ok := m.dataInfoErr[group]; ok {
return nil, err
@@ -208,3 +228,40 @@ func TestInspectAll_MultipleGroups(t *testing.T) {
assert.Equal(t, "CATALOG_STREAM", resp.Groups[1].Catalog)
assert.Nil(t, resp.Groups[1].DataInfo)
}
+
+func TestInspectAll_RunsGroupsInParallel(t *testing.T) {
+ const groupCount = 8
+ const sleep = 80 * time.Millisecond
+ groups := make([]*commonv1.Group, 0, groupCount)
+ slow := make(map[string]time.Duration, groupCount)
+ for i := range groupCount {
+ name := fmt.Sprintf("g_%d", i)
+ groups = append(groups, &commonv1.Group{
+ Metadata: &commonv1.Metadata{Name: name},
+ Catalog: commonv1.Catalog_CATALOG_STREAM,
+ })
+ slow[name] = sleep
+ }
+ repo := &mockMetadataRepo{
+ groupRegistry: &mockGroupRegistry{groups: groups},
+ slowGroups: slow,
+ }
+ s := &server{
+ log: logger.GetLogger("test"),
+ metadataRepo: repo,
+ }
+
+ start := time.Now()
+ resp, err := s.InspectAll(context.Background(), &fodcv1.InspectAllRequest{})
+ elapsed := time.Since(start)
+ require.NoError(t, err)
+ require.Len(t, resp.Groups, groupCount)
+
+ // Sequential execution would take groupCount*sleep; parallel must finish
+ // in roughly one sleep interval. Allow 4x headroom for CI jitter.
+ assert.Less(t, elapsed, 4*sleep, "InspectAll must run groups in parallel; total wall time was %s", elapsed)
+ assert.Equal(t, int32(groupCount), repo.collectStarts.Load())
+ assert.GreaterOrEqual(t, repo.concurrentMax.Load(), int32(2),
+ "at least two CollectDataInfo invocations must overlap; observed max concurrency was %d",
+ repo.concurrentMax.Load())
+}
(skywalking-banyandb) 02/24: Fix property schema client connection not stable after data node restarted (#1038)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit a14b7d486be4d1f4fae2b2057ab7b78fa073f31c
Author: mrproliu <74...@qq.com>
AuthorDate: Tue Mar 31 22:02:45 2026 +0800
Fix property schema client connection not stable after data node restarted (#1038)
* Fix property schema client connection not stable after data node restarted
---
banyand/backup/lifecycle/steps.go | 3 +-
banyand/metadata/client.go | 96 +++++------
banyand/metadata/schema/property/client.go | 189 +++++++++++++++------
banyand/metadata/schema/property/client_test.go | 118 +++++++++++++-
banyand/queue/pub/pub.go | 2 +-
docs/operation/configuration.md | 11 ++
pkg/grpchelper/connmanager.go | 74 ++++++---
pkg/grpchelper/connmanager_test.go | 208 ++++++++++++++++++++++++
pkg/grpchelper/helpers_test.go | 21 +++
9 files changed, 592 insertions(+), 130 deletions(-)
diff --git a/banyand/backup/lifecycle/steps.go b/banyand/backup/lifecycle/steps.go
index deba6d767..700897840 100644
--- a/banyand/backup/lifecycle/steps.go
+++ b/banyand/backup/lifecycle/steps.go
@@ -101,6 +101,7 @@ func (gc *GroupConfig) Close() {
}
}
+//nolint:contextcheck // health check goroutine uses context.Background()
func parseGroup(
g *commonv1.Group, nodeLabels map[string]string, nodes []*databasev1.Node,
l *logger.Logger, metadata metadata.Repo, clusterStateMgr *clusterStateManager,
@@ -148,7 +149,7 @@ func parseGroup(
if ok, _ := nodeSel.OnInit([]schema.Kind{schema.KindGroup}); !ok {
return nil, fmt.Errorf("failed to initialize node selector for group %s", g.Metadata.Name)
}
- client := pub.NewWithoutMetadata()
+ client := pub.NewWithoutMetadata() //nolint:contextcheck // health check goroutine uses context.Background()
if g.Catalog == commonv1.Catalog_CATALOG_STREAM {
_ = grpc.NewClusterNodeRegistry(data.TopicStreamWrite, client, nodeSel)
} else {
diff --git a/banyand/metadata/client.go b/banyand/metadata/client.go
index 0608e19ce..9ae0dd359 100644
--- a/banyand/metadata/client.go
+++ b/banyand/metadata/client.go
@@ -98,44 +98,45 @@ func NewClient(toRegisterNode, forceRegisterNode bool) (Service, error) {
}
type clientService struct {
- schemaRegistry schema.Registry
- nodeDiscoveryRegistry schema.NodeDiscovery
- omr observability.MetricsRegistry
- dataBroadcaster bus.Broadcaster
- liaisonBroadcaster bus.Broadcaster
- infoCollectorRegistry *schema.InfoCollectorRegistry
- closer *run.Closer
- nodeInfo *databasev1.Node
- etcdTLSCertFile string
- etcdPassword string
- etcdTLSCAFile string
- etcdUsername string
- etcdTLSKeyFile string
- namespace string
- nodeDiscoveryMode string
- schemaRegistryMode string
- filePath string
- propertySchemaClientCACert string
- dnsCACertPaths []string
- dnsSRVAddresses []string
- endpoints []string
- registryTimeout time.Duration
- dnsFetchInitInterval time.Duration
- dnsFetchInitDuration time.Duration
- dnsFetchInterval time.Duration
- grpcTimeout time.Duration
- etcdFullSyncInterval time.Duration
- propertySchemaSyncInterval time.Duration
- fileFetchInterval time.Duration
- fileRetryInitialInterval time.Duration
- fileRetryMaxInterval time.Duration
- propertySchemaMaxRecvSize run.Bytes
- fileRetryMultiplier float64
- nodeInfoMux sync.Mutex
- forceRegisterNode bool
- toRegisterNode bool
- dnsTLSEnabled bool
- propertySchemaClientTLS bool
+ schemaRegistry schema.Registry
+ nodeDiscoveryRegistry schema.NodeDiscovery
+ omr observability.MetricsRegistry
+ dataBroadcaster bus.Broadcaster
+ liaisonBroadcaster bus.Broadcaster
+ infoCollectorRegistry *schema.InfoCollectorRegistry
+ closer *run.Closer
+ nodeInfo *databasev1.Node
+ etcdTLSCertFile string
+ etcdPassword string
+ etcdTLSCAFile string
+ etcdUsername string
+ etcdTLSKeyFile string
+ namespace string
+ nodeDiscoveryMode string
+ schemaRegistryMode string
+ filePath string
+ propertySchemaClientCACert string
+ dnsCACertPaths []string
+ dnsSRVAddresses []string
+ endpoints []string
+ registryTimeout time.Duration
+ dnsFetchInitInterval time.Duration
+ dnsFetchInitDuration time.Duration
+ dnsFetchInterval time.Duration
+ grpcTimeout time.Duration
+ etcdFullSyncInterval time.Duration
+ propertySchemaSyncInterval time.Duration
+ propertySchemaHealthCheckInterval time.Duration
+ fileFetchInterval time.Duration
+ fileRetryInitialInterval time.Duration
+ fileRetryMaxInterval time.Duration
+ propertySchemaMaxRecvSize run.Bytes
+ fileRetryMultiplier float64
+ nodeInfoMux sync.Mutex
+ forceRegisterNode bool
+ toRegisterNode bool
+ dnsTLSEnabled bool
+ propertySchemaClientTLS bool
}
func (s *clientService) SchemaRegistry() schema.Registry {
@@ -159,6 +160,8 @@ func (s *clientService) FlagSet() *run.FlagSet {
"Schema registry mode: 'etcd' for etcd-based storage, 'property' for property-based storage")
fs.DurationVar(&s.propertySchemaSyncInterval, "schema-property-client-sync-interval", property.DefaultSyncInterval,
"Polling interval for property-based schema sync")
+ fs.DurationVar(&s.propertySchemaHealthCheckInterval, "schema-property-client-health-check-interval", property.DefaultHealthCheckInterval,
+ "Interval for periodic connection health checks to schema servers. 0 uses default, negative disables")
s.propertySchemaMaxRecvSize = defaultRecvSize
fs.VarP(&s.propertySchemaMaxRecvSize, "schema-property-client-max-recv-msg-size", "",
"Max gRPC receive message size for property schema client")
@@ -426,14 +429,15 @@ func (s *clientService) initPropertySchemaRegistry(ctx context.Context, l *logge
}
}
cfg := &property.ClientConfig{
- GRPCTimeout: s.grpcTimeout,
- SyncInterval: s.propertySchemaSyncInterval,
- OMR: s.omr,
- CurNode: currentNode,
- NodeRegistry: s.NodeRegistry(),
- MaxRecvMsgSize: int(s.propertySchemaMaxRecvSize),
- TLSEnabled: s.propertySchemaClientTLS,
- CACertPath: s.propertySchemaClientCACert,
+ GRPCTimeout: s.grpcTimeout,
+ SyncInterval: s.propertySchemaSyncInterval,
+ HealthCheckInterval: s.propertySchemaHealthCheckInterval,
+ OMR: s.omr,
+ CurNode: currentNode,
+ NodeRegistry: s.NodeRegistry(),
+ MaxRecvMsgSize: int(s.propertySchemaMaxRecvSize),
+ TLSEnabled: s.propertySchemaClientTLS,
+ CACertPath: s.propertySchemaClientCACert,
}
for attempt := 1; attempt <= propertyRegistryInitRetryCount; attempt++ {
registry, createErr := property.NewSchemaRegistryClient(cfg) //nolint:contextcheck // healthCheck uses its own 2s timeout via context.Background()
diff --git a/banyand/metadata/schema/property/client.go b/banyand/metadata/schema/property/client.go
index 19b6d42fe..0d7f8d381 100644
--- a/banyand/metadata/schema/property/client.go
+++ b/banyand/metadata/schema/property/client.go
@@ -54,6 +54,10 @@ const (
DefaultSyncInterval = 30 * time.Second
// DefaultInitWaitTime is the default maximum time to wait for at least one schema server to become active during initialization.
DefaultInitWaitTime = time.Minute
+ // DefaultHealthCheckInterval is the default interval for periodic connection health checks.
+ DefaultHealthCheckInterval = 10 * time.Second
+ // defaultSyncTimeout is the default timeout for waiting for a sync response from a single node.
+ defaultSyncTimeout = 30 * time.Second
)
var _ schema.Registry = (*SchemaRegistry)(nil)
@@ -62,16 +66,19 @@ var errNoActiveServers = fmt.Errorf("no active schema servers available")
// ClientConfig holds configuration for the property-based schema registry client.
type ClientConfig struct {
- OMR observability.MetricsRegistry
- NodeRegistry schema.Node
- CurNode *databasev1.Node
- CACertPath string
- GRPCTimeout time.Duration
- SyncInterval time.Duration
- InitWaitTime time.Duration
- FullReconcileEvery uint64
- MaxRecvMsgSize int
- TLSEnabled bool
+ OMR observability.MetricsRegistry
+ NodeRegistry schema.Node
+ CurNode *databasev1.Node
+ CACertPath string
+ GRPCTimeout time.Duration
+ SyncInterval time.Duration
+ SyncTimeout time.Duration
+ WatchMaxBackoff time.Duration
+ InitWaitTime time.Duration
+ HealthCheckInterval time.Duration
+ FullReconcileEvery uint64
+ MaxRecvMsgSize int
+ TLSEnabled bool
}
type syncRequest struct {
@@ -79,10 +86,14 @@ type syncRequest struct {
tagProjection []string
}
+type syncMessage struct {
+ responseCh chan []*digestEntry
+ syncRequest
+}
+
type watchSession struct {
cancelFn context.CancelFunc
- syncReqCh chan *syncRequest
- syncResCh chan []*digestEntry
+ syncReqCh chan *syncMessage
}
type digestEntry struct {
@@ -107,6 +118,8 @@ type SchemaRegistry struct {
handlers map[schema.Kind][]schema.EventHandler
watchSessions map[string]*watchSession
syncInterval time.Duration
+ syncTimeout time.Duration
+ watchMaxBackoff time.Duration
fullReconcileEvery uint64
syncRound uint64
mux sync.RWMutex
@@ -129,10 +142,17 @@ func NewSchemaRegistryClient(cfg *ClientConfig) (*SchemaRegistry, error) {
caCertReloader: caCertReloader,
tlsEnabled: cfg.TLSEnabled,
}
+ healthCheckInterval := cfg.HealthCheckInterval
+ if healthCheckInterval == 0 {
+ healthCheckInterval = DefaultHealthCheckInterval
+ } else if healthCheckInterval < 0 {
+ healthCheckInterval = 0 // negative means disabled
+ }
connMgr := grpchelper.NewConnManager[*schemaClient](grpchelper.ConnManagerConfig[*schemaClient]{
- Handler: handler,
- Logger: l,
- MaxRecvMsgSize: cfg.MaxRecvMsgSize,
+ Handler: handler,
+ Logger: l,
+ MaxRecvMsgSize: cfg.MaxRecvMsgSize,
+ HealthCheckInterval: healthCheckInterval,
})
syncInterval := cfg.SyncInterval
if syncInterval == 0 {
@@ -146,6 +166,14 @@ func NewSchemaRegistryClient(cfg *ClientConfig) (*SchemaRegistry, error) {
if fullReconcileEvery <= 0 {
fullReconcileEvery = DefaultFullReconcileEvery
}
+ syncTimeout := cfg.SyncTimeout
+ if syncTimeout == 0 {
+ syncTimeout = defaultSyncTimeout
+ }
+ watchMaxBackoff := cfg.WatchMaxBackoff
+ if watchMaxBackoff == 0 {
+ watchMaxBackoff = defaultWatchMaxBackoff
+ }
reg := &SchemaRegistry{
connMgr: connMgr,
closer: run.NewCloser(1),
@@ -155,6 +183,8 @@ func NewSchemaRegistryClient(cfg *ClientConfig) (*SchemaRegistry, error) {
handlers: make(map[schema.Kind][]schema.EventHandler),
watchSessions: make(map[string]*watchSession),
syncInterval: syncInterval,
+ syncTimeout: syncTimeout,
+ watchMaxBackoff: watchMaxBackoff,
fullReconcileEvery: fullReconcileEvery,
}
handler.registry = reg
@@ -180,8 +210,6 @@ func NewSchemaRegistryClient(cfg *ClientConfig) (*SchemaRegistry, error) {
}
// Wait for at least one schema server to become active.
- // OnAddOrUpdate health-checks synchronously; if it fails, a background
- // retry goroutine retries backoff. Poll until active or timeout.
if connMgr.ActiveCount() == 0 {
waitDeadline := time.Now().Add(initWaitTime)
for connMgr.ActiveCount() == 0 && time.Now().Before(waitDeadline) {
@@ -939,7 +967,7 @@ func (r *SchemaRegistry) RegisterHandler(name string, kind schema.Kind, handler
}
}
r.l.Info().Str("name", name).Interface("kinds", kinds).Msg("registering property schema handler")
- handler.OnInit(kinds)
+ r.initHandlerWithRetry(name, handler, kinds)
r.mux.Lock()
defer r.mux.Unlock()
for _, ki := range kinds {
@@ -947,6 +975,34 @@ func (r *SchemaRegistry) RegisterHandler(name string, kind schema.Kind, handler
}
}
+func (r *SchemaRegistry) initHandlerWithRetry(name string, handler schema.EventHandler, kinds []schema.Kind) {
+ deadline := time.Now().Add(time.Minute)
+ var lastErr interface{}
+ for {
+ if tryCallOnInit(handler, kinds, &lastErr) {
+ return
+ }
+ if time.Now().After(deadline) {
+ r.l.Panic().Str("name", name).Interface("error", lastErr).
+ Msg("handler OnInit failed after 1m, giving up")
+ }
+ r.l.Warn().Str("name", name).Interface("error", lastErr).
+ Msg("handler OnInit panicked due to transient error, retrying in 1s")
+ time.Sleep(time.Second)
+ }
+}
+
+func tryCallOnInit(handler schema.EventHandler, kinds []schema.Kind, lastErr *interface{}) (ok bool) {
+ defer func() {
+ if rec := recover(); rec != nil {
+ *lastErr = rec
+ ok = false
+ }
+ }()
+ handler.OnInit(kinds)
+ return true
+}
+
// Start starts a single goroutine that periodically syncs schemas.
func (r *SchemaRegistry) Start(ctx context.Context) error {
if r.caCertReloader != nil {
@@ -1017,8 +1073,7 @@ func (r *SchemaRegistry) launchWatch(nodeName string, client *schemaClient) {
ctx, cancel := context.WithCancel(r.closer.Ctx())
session := &watchSession{
cancelFn: cancel,
- syncReqCh: make(chan *syncRequest, 1),
- syncResCh: make(chan []*digestEntry, 1),
+ syncReqCh: make(chan *syncMessage, 1),
}
r.watchSessions[nodeName] = session
r.l.Debug().Str("node", nodeName).Msg("launchWatch: starting new watch session")
@@ -1045,7 +1100,8 @@ func (r *SchemaRegistry) stopWatch(nodeName string) {
const (
watchInitialBackoff = time.Second
- watchMaxBackoff = 30 * time.Second
+ // defaultWatchMaxBackoff is the default maximum backoff for watch stream reconnection.
+ defaultWatchMaxBackoff = 30 * time.Second
)
func (r *SchemaRegistry) watchLoop(ctx context.Context, nodeName string,
@@ -1073,8 +1129,8 @@ func (r *SchemaRegistry) watchLoop(ctx context.Context, nodeName string,
case <-time.After(backoff):
}
backoff *= 2
- if backoff > watchMaxBackoff {
- backoff = watchMaxBackoff
+ if backoff > r.watchMaxBackoff {
+ backoff = r.watchMaxBackoff
}
}
}
@@ -1113,6 +1169,7 @@ func (r *SchemaRegistry) processWatchSession(ctx context.Context,
var inSync bool
var metadataOnly bool
var digests []*digestEntry
+ var currentResponseCh chan []*digestEntry
for {
select {
@@ -1132,18 +1189,20 @@ func (r *SchemaRegistry) processWatchSession(ctx context.Context,
inSync = true
metadataOnly = len(req.tagProjection) > 0
digests = nil
+ currentResponseCh = req.responseCh
case resp, ok := <-recvCh:
if !ok {
return <-recvErrCh
}
if resp.EventType == schemav1.SchemaEventType_SCHEMA_EVENT_TYPE_REPLAY_DONE {
r.l.Debug().Bool("inSync", inSync).Msg("processWatchSession: received REPLAY_DONE")
- if inSync {
+ if inSync && currentResponseCh != nil {
r.l.Debug().Int("digestCount", len(digests)).Msg("processWatchSession: sync replay completed, sending digests")
- session.syncResCh <- digests
+ currentResponseCh <- digests
digests = nil
inSync = false
metadataOnly = false
+ currentResponseCh = nil
}
continue
}
@@ -1219,7 +1278,9 @@ func (r *SchemaRegistry) performIncrementalSync(ctx context.Context) {
criteria: buildRevisionCriteria(maxRev),
}
r.l.Debug().Int64("maxRevision", maxRev).Msg("incremental sync: requesting changes")
- r.sendSyncRequest(ctx, sessions, req)
+ if _, err := r.sendSyncRequest(ctx, sessions, req); err != nil {
+ r.l.Warn().Err(err).Msg("incremental sync: sync request partially failed")
+ }
}
func (r *SchemaRegistry) getActiveSessions() map[string]*watchSession {
@@ -1234,29 +1295,43 @@ func (r *SchemaRegistry) getActiveSessions() map[string]*watchSession {
func (r *SchemaRegistry) sendSyncRequest(ctx context.Context,
sessions map[string]*watchSession, req *syncRequest,
-) map[string][]*digestEntry {
+) (map[string][]*digestEntry, error) {
r.l.Debug().Int("sessionCount", len(sessions)).Msg("sendSyncRequest: dispatching to watch sessions")
- sent := make(map[string]*watchSession, len(sessions))
+ sentChs := make(map[string]chan []*digestEntry, len(sessions))
+ var skipped int
for name, s := range sessions {
+ msg := &syncMessage{
+ syncRequest: *req,
+ responseCh: make(chan []*digestEntry, 1),
+ }
select {
- case s.syncReqCh <- req:
- sent[name] = s
+ case s.syncReqCh <- msg:
+ sentChs[name] = msg.responseCh
default:
+ skipped++
r.l.Warn().Str("node", name).Msg("sendSyncRequest: channel full, skipping session")
}
}
- allDigests := make(map[string][]*digestEntry, len(sent))
- for name, s := range sent {
+ var timedOut int
+ allDigests := make(map[string][]*digestEntry, len(sentChs))
+ for name, ch := range sentChs {
select {
- case digests := <-s.syncResCh:
+ case digests := <-ch:
allDigests[name] = digests
- case <-time.After(30 * time.Second):
+ r.l.Debug().Str("node", name).Int("digestCount", len(digests)).Msg("sendSyncRequest: received digests")
+ case <-time.After(r.syncTimeout):
+ timedOut++
r.l.Warn().Str("node", name).Msg("sync timeout")
case <-ctx.Done():
- return nil
+ return nil, ctx.Err()
}
}
- return allDigests
+ r.l.Debug().Int("totalSessions", len(sessions)).Int("sent", len(sentChs)).Int("responded", len(allDigests)).Msg("sendSyncRequest: completed")
+ if skipped > 0 || timedOut > 0 {
+ return allDigests, fmt.Errorf("sync partially failed: %d skipped, %d timed out out of %d sessions",
+ skipped, timedOut, len(sessions))
+ }
+ return allDigests, nil
}
func (r *SchemaRegistry) performFullSync(ctx context.Context) {
@@ -1268,14 +1343,21 @@ func (r *SchemaRegistry) performFullSync(ctx context.Context) {
req := &syncRequest{
tagProjection: basicTagKeys,
}
- allDigests := r.sendSyncRequest(ctx, sessions, req)
+ allDigests, err := r.sendSyncRequest(ctx, sessions, req)
if allDigests == nil {
return
}
+ if err != nil {
+ // Partial failure: some sessions timed out or were skipped.
+ // Use whatever digests were received for the add/update path,
+ // but skip the deletion reconciliation to avoid removing entries
+ // that may exist on unreachable nodes.
+ r.l.Warn().Err(err).Msg("performFullSync: sync request partially failed, skipping deletion reconciliation")
+ }
merged := r.mergeDigests(allDigests)
cachedEntries := r.cache.GetAllEntries()
- r.l.Debug().Int("mergedCount", len(merged)).Int("cachedCount", len(cachedEntries)).
+ r.l.Debug().Int("respondedNodes", len(allDigests)).Int("mergedCount", len(merged)).Int("cachedCount", len(cachedEntries)).
Msg("performFullSync: comparing server digests with local cache")
for propID, d := range merged {
@@ -1297,30 +1379,35 @@ func (r *SchemaRegistry) performFullSync(ctx context.Context) {
r.l.Warn().Str("kind", d.kind).Msg("full sync: unknown kind")
continue
}
- r.l.Debug().Str("propID", propID).Stringer("kind", kind).
- Int64("serverRevision", d.revision).Msg("performFullSync: fetching updated schema from server")
- prop, err := r.getSchema(ctx, kind, d.group, d.name)
- if err != nil {
- r.l.Warn().Err(err).Str("propID", propID).Msg("full sync: failed to get schema")
+ r.l.Info().Str("propID", propID).Stringer("kind", kind).
+ Int64("serverRevision", d.revision).
+ Bool("localMissing", localEntry == nil).
+ Msg("performFullSync: fetching updated schema from server")
+ prop, getErr := r.getSchema(ctx, kind, d.group, d.name)
+ if getErr != nil {
+ r.l.Warn().Err(getErr).Str("propID", propID).Msg("full sync: failed to get schema")
continue
}
if prop == nil {
continue
}
- md, err := ToSchema(kind, prop)
- if err != nil {
- r.l.Warn().Err(err).Str("propID", propID).Msg("full sync: failed to convert property")
+ md, convertErr := ToSchema(kind, prop)
+ if convertErr != nil {
+ r.l.Warn().Err(convertErr).Str("propID", propID).Msg("full sync: failed to convert property")
continue
}
r.processInitialResourceFromProperty(kind, prop, md.Spec.(proto.Message))
}
}
- for propID, entry := range cachedEntries {
- if _, exists := merged[propID]; !exists {
- r.l.Debug().Str("propID", propID).Stringer("kind", entry.kind).
- Msg("performFullSync: cached entry not found on server, deleting")
- r.handleDeletion(entry.kind, propID, entry, entry.latestUpdateAt)
+ if err == nil {
+ for propID, entry := range cachedEntries {
+ if _, exists := merged[propID]; !exists {
+ r.l.Warn().Str("propID", propID).Stringer("kind", entry.kind).
+ Str("group", entry.group).Str("name", entry.name).
+ Msg("performFullSync: cached entry not found on server, deleting")
+ r.handleDeletion(entry.kind, propID, entry, entry.latestUpdateAt)
+ }
}
}
}
diff --git a/banyand/metadata/schema/property/client_test.go b/banyand/metadata/schema/property/client_test.go
index 4986e9192..cbb997649 100644
--- a/banyand/metadata/schema/property/client_test.go
+++ b/banyand/metadata/schema/property/client_test.go
@@ -1337,11 +1337,7 @@ func (s *recordingSchemaServer) DeleteSchema(_ context.Context, req *schemav1.De
return &schemav1.DeleteSchemaResponse{Found: false}, nil
}
-func startRecordingServer(t *testing.T) (*recordingSchemaServer, string) {
- t.Helper()
- mock := newRecordingSchemaServer()
- lis, lisErr := net.Listen("tcp", "127.0.0.1:0")
- require.NoError(t, lisErr)
+func serveRecordingServer(mock *recordingSchemaServer, lis net.Listener) *grpc.Server {
srv := grpc.NewServer()
schemav1.RegisterSchemaManagementServiceServer(srv, mock)
schemav1.RegisterSchemaUpdateServiceServer(srv, mock)
@@ -1349,6 +1345,15 @@ func startRecordingServer(t *testing.T) (*recordingSchemaServer, string) {
healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(srv, healthSrv)
go func() { _ = srv.Serve(lis) }()
+ return srv
+}
+
+func startRecordingServer(t *testing.T) (*recordingSchemaServer, string) {
+ t.Helper()
+ mock := newRecordingSchemaServer()
+ lis, lisErr := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, lisErr)
+ srv := serveRecordingServer(mock, lis)
t.Cleanup(func() { srv.GracefulStop() })
return mock, lis.Addr().String()
}
@@ -1567,6 +1572,109 @@ func deleteSchemaProperty(t *testing.T, mgmt schemav1.SchemaManagementServiceCli
require.NoError(t, err)
}
+func TestFullSyncServerDown(t *testing.T) {
+ t.Run("groups_preserved_when_server_stops", func(t *testing.T) {
+ // Verifies that when the schema server stops (simulating hot node restart),
+ // performFullSync does NOT delete locally cached groups because
+ // sendSyncRequest returns an error on timeout.
+ _, addr, stopFn := startTestSchemaServerFull(t)
+ mgmt := rawMgmtClient(t, addr)
+
+ insertSchemaProperty(t, mgmt, buildMockGroupProperty(t))
+ insertSchemaProperty(t, mgmt, buildMockStreamProperty(t, "sv-stream", time.Now()))
+
+ reg := newTestRegistryWithConfig(t, &property.ClientConfig{
+ SyncInterval: 200 * time.Millisecond,
+ SyncTimeout: 2 * time.Second,
+ FullReconcileEvery: 1,
+ }, addr)
+ handler := &testEventHandler{}
+ reg.RegisterHandler("test-handler", schema.KindGroup|schema.KindStream, handler)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+ require.NoError(t, reg.Start(ctx))
+
+ waitForAddOrUpdateEvent(t, handler, schema.KindGroup, "test-group")
+ waitForAddOrUpdateEvent(t, handler, schema.KindStream, "sv-stream")
+ initialDeleteCount := len(handler.getDeleteEvents())
+
+ // Stop the server. The watch session stays "active" but broken.
+ stopFn()
+
+ // Wait for the sync timeout (2s) to fire plus some buffer.
+ time.Sleep(5 * time.Second)
+
+ // Verify NO group delete events were fired.
+ deleteEvents := handler.getDeleteEvents()
+ groupDeleteCount := 0
+ for _, evt := range deleteEvents[initialDeleteCount:] {
+ if evt.Kind == schema.KindGroup && evt.Name == "test-group" {
+ groupDeleteCount++
+ }
+ }
+ assert.Equal(t, 0, groupDeleteCount, "Groups should NOT be deleted when server is unreachable")
+ })
+
+ t.Run("groups_recovered_after_server_restart", func(t *testing.T) {
+ // Verifies that after server stops and restarts on the same address,
+ // the watch session reconnects and groups remain consistent.
+ lis, lisErr := net.Listen("tcp", "127.0.0.1:0")
+ require.NoError(t, lisErr)
+ addr := lis.Addr().String()
+
+ mock := newRecordingSchemaServer()
+ mock.addSchema(buildMockGroupProperty(t))
+ mock.addSchema(buildMockStreamProperty(t, "sv-stream", time.Now()))
+ srv := serveRecordingServer(mock, lis)
+
+ reg := newTestRegistryWithConfig(t, &property.ClientConfig{
+ SyncInterval: 200 * time.Millisecond,
+ SyncTimeout: 2 * time.Second,
+ WatchMaxBackoff: 2 * time.Second,
+ FullReconcileEvery: 1,
+ }, addr)
+ handler := &testEventHandler{}
+ reg.RegisterHandler("test-handler", schema.KindGroup|schema.KindStream, handler)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ t.Cleanup(cancel)
+ require.NoError(t, reg.Start(ctx))
+
+ waitForAddOrUpdateEvent(t, handler, schema.KindGroup, "test-group")
+ waitForAddOrUpdateEvent(t, handler, schema.KindStream, "sv-stream")
+ initialDeleteCount := len(handler.getDeleteEvents())
+
+ // Stop the server (hard stop to avoid blocking on bidi streams).
+ srv.Stop()
+
+ // Wait for sync timeout (2s) to fire. Groups should NOT be deleted.
+ time.Sleep(5 * time.Second)
+ for _, evt := range handler.getDeleteEvents()[initialDeleteCount:] {
+ require.NotEqual(t, schema.KindGroup, evt.Kind,
+ "Group should not be deleted during server outage")
+ }
+
+ // Restart server on same address with same data.
+ lis2, lisErr2 := net.Listen("tcp", addr)
+ require.NoError(t, lisErr2)
+ mock2 := newRecordingSchemaServer()
+ mock2.addSchema(buildMockGroupProperty(t))
+ mock2.addSchema(buildMockStreamProperty(t, "sv-stream", time.Now()))
+ srv2 := serveRecordingServer(mock2, lis2)
+ t.Cleanup(func() { srv2.Stop() })
+
+ // Wait for watch reconnection (backoff up to 2s) and a successful full sync.
+ time.Sleep(5 * time.Second)
+
+ // Verify no group was deleted during the entire process.
+ for _, evt := range handler.getDeleteEvents()[initialDeleteCount:] {
+ assert.NotEqual(t, schema.KindGroup, evt.Kind,
+ "Group should not be deleted after server restart")
+ }
+ })
+}
+
func TestWatchPush(t *testing.T) {
t.Run("watch_push_insert", func(t *testing.T) {
srv, addr, _ := startTestSchemaServerFull(t)
diff --git a/banyand/queue/pub/pub.go b/banyand/queue/pub/pub.go
index 05043ac0b..e7b975176 100644
--- a/banyand/queue/pub/pub.go
+++ b/banyand/queue/pub/pub.go
@@ -388,7 +388,7 @@ func (p *pub) PreRun(context.Context) error {
p.log = logger.GetLogger("server-queue-pub-" + p.prefix)
// Initialize connection manager with the pub as the handler
- p.connMgr = grpchelper.NewConnManager(grpchelper.ConnManagerConfig[*client]{
+ p.connMgr = grpchelper.NewConnManager(grpchelper.ConnManagerConfig[*client]{ //nolint:contextcheck // health check runs in background goroutine
Handler: p,
Logger: p.log,
RetryPolicy: p.retryPolicy,
diff --git a/docs/operation/configuration.md b/docs/operation/configuration.md
index 7c7b38a50..0782a000b 100644
--- a/docs/operation/configuration.md
+++ b/docs/operation/configuration.md
@@ -133,6 +133,17 @@ The following flags are used to configure the memory protector:
- `--dst-slow-query duration`: distributed slow query threshold, 0 means no slow query log. This is only used for the liaison server (default: 0).
- `--slow-query duration`: slow query threshold, 0 means no slow query log. This is only used for the data and standalone server (default: 0).
+### Schema Registry
+
+The following flags configure the property-based schema registry used for metadata synchronization between nodes:
+
+- `--schema-registry-mode string`: Schema registry mode: `etcd` for etcd-based storage, `property` for property-based storage (default: "property").
+- `--schema-property-client-sync-interval duration`: Polling interval for property-based schema sync (default: 30s).
+- `--schema-property-client-health-check-interval duration`: Interval for periodic connection health checks to schema servers. Set to 0 to use the default interval (10s), or to a negative value to disable.
+- `--schema-property-client-max-recv-msg-size bytes`: Max gRPC receive message size for property schema client.
+- `--schema-property-client-tls`: Enable TLS for property schema client connections.
+- `--schema-property-client-ca-cert string`: CA certificate file to verify the property schema server.
+
### Other
- `-n, --name string`: Name of this service.
diff --git a/pkg/grpchelper/connmanager.go b/pkg/grpchelper/connmanager.go
index 4e9793679..eb1406bac 100644
--- a/pkg/grpchelper/connmanager.go
+++ b/pkg/grpchelper/connmanager.go
@@ -60,27 +60,29 @@ type ConnectionHandler[C Client] interface {
// ConnManagerConfig holds configuration for ConnManager.
type ConnManagerConfig[C Client] struct {
- Handler ConnectionHandler[C]
- Logger *logger.Logger
- RetryPolicy string
- ExtraDialOpts []grpc.DialOption
- MaxRecvMsgSize int
- HealthCheckTimeout time.Duration
+ Handler ConnectionHandler[C]
+ Logger *logger.Logger
+ RetryPolicy string
+ ExtraDialOpts []grpc.DialOption
+ HealthCheckTimeout time.Duration
+ HealthCheckInterval time.Duration
+ MaxRecvMsgSize int
}
// ConnManager manages gRPC connections with health checking, circuit breaking, and eviction.
type ConnManager[C Client] struct {
- handler ConnectionHandler[C]
- log *logger.Logger
- registered map[string]*databasev1.Node
- active map[string]*managedNode[C]
- evictable map[string]evictNode
- cbStates map[string]*circuitState
- closer *run.Closer
- dialOpts []grpc.DialOption
- healthCheckTimeout time.Duration
- mu sync.RWMutex
- cbMu sync.RWMutex
+ handler ConnectionHandler[C]
+ log *logger.Logger
+ registered map[string]*databasev1.Node
+ active map[string]*managedNode[C]
+ evictable map[string]evictNode
+ cbStates map[string]*circuitState
+ closer *run.Closer
+ dialOpts []grpc.DialOption
+ healthCheckTimeout time.Duration
+ healthCheckInterval time.Duration
+ mu sync.RWMutex
+ cbMu sync.RWMutex
}
type managedNode[C Client] struct {
@@ -109,15 +111,19 @@ func NewConnManager[C Client](cfg ConnManagerConfig[C]) *ConnManager[C] {
healthCheckTimeout = 2 * time.Second
}
m := &ConnManager[C]{
- handler: cfg.Handler,
- log: cfg.Logger,
- registered: make(map[string]*databasev1.Node),
- active: make(map[string]*managedNode[C]),
- evictable: make(map[string]evictNode),
- cbStates: make(map[string]*circuitState),
- closer: run.NewCloser(1),
- dialOpts: dialOpts,
- healthCheckTimeout: healthCheckTimeout,
+ handler: cfg.Handler,
+ log: cfg.Logger,
+ registered: make(map[string]*databasev1.Node),
+ active: make(map[string]*managedNode[C]),
+ evictable: make(map[string]evictNode),
+ cbStates: make(map[string]*circuitState),
+ closer: run.NewCloser(1),
+ dialOpts: dialOpts,
+ healthCheckTimeout: healthCheckTimeout,
+ healthCheckInterval: cfg.HealthCheckInterval,
+ }
+ if m.healthCheckInterval > 0 && m.closer.AddRunning() {
+ go m.periodicHealthCheck()
}
return m
}
@@ -499,6 +505,22 @@ func (m *ConnManager[C]) checkHealthAndReconnect(conn *grpc.ClientConn, node *da
return false
}
+func (m *ConnManager[C]) periodicHealthCheck() {
+ defer m.closer.Done()
+ ticker := time.NewTicker(m.healthCheckInterval)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-ticker.C:
+ for _, name := range m.ActiveNames() {
+ m.FailoverNode(name)
+ }
+ case <-m.closer.CloseNotify():
+ return
+ }
+ }
+}
+
func (m *ConnManager[C]) healthCheck(node string, conn *grpc.ClientConn) bool {
var resp *grpc_health_v1.HealthCheckResponse
if requestErr := Request(context.Background(), m.healthCheckTimeout, func(rpcCtx context.Context) (err error) {
diff --git a/pkg/grpchelper/connmanager_test.go b/pkg/grpchelper/connmanager_test.go
index f85303fbf..7c99e6e6a 100644
--- a/pkg/grpchelper/connmanager_test.go
+++ b/pkg/grpchelper/connmanager_test.go
@@ -18,12 +18,23 @@
package grpchelper
import (
+ "net"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "google.golang.org/grpc"
"google.golang.org/grpc/codes"
+ "google.golang.org/grpc/credentials/insecure"
+ "google.golang.org/grpc/health"
+ "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/status"
+
+ commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+ databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
+ "github.com/apache/skywalking-banyandb/pkg/logger"
+ testflags "github.com/apache/skywalking-banyandb/pkg/test/flags"
)
func addTestClient(m *ConnManager[*mockClient], node string) {
@@ -113,3 +124,200 @@ func TestExecute_ClientNotFound(t *testing.T) {
assert.ErrorIs(t, execErr, ErrClientNotFound)
assert.Contains(t, execErr.Error(), node)
}
+
+func startHealthServerOnAddr(t *testing.T, addr string) (string, func()) {
+ t.Helper()
+ lis, lisErr := net.Listen("tcp", addr)
+ require.NoError(t, lisErr)
+ srv := grpc.NewServer()
+ healthSrv := health.NewServer()
+ healthSrv.SetServingStatus("", grpc_health_v1.HealthCheckResponse_SERVING)
+ grpc_health_v1.RegisterHealthServer(srv, healthSrv)
+ go func() { _ = srv.Serve(lis) }()
+ return lis.Addr().String(), func() { srv.Stop() }
+}
+
+func startHealthServer(t *testing.T) (string, func()) {
+ t.Helper()
+ return startHealthServerOnAddr(t, "127.0.0.1:0")
+}
+
+func buildNode(name, addr string) *databasev1.Node {
+ return &databasev1.Node{
+ Metadata: &commonv1.Metadata{Name: name},
+ GrpcAddress: addr,
+ }
+}
+
+// mockHandlerWithInsecure extends mockHandler with insecure dial options for real gRPC connections.
+type mockHandlerWithInsecure struct {
+ mockHandler
+}
+
+func (h *mockHandlerWithInsecure) GetDialOptions() ([]grpc.DialOption, error) {
+ return []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}, nil
+}
+
+func newHealthCheckConnManager(t *testing.T, interval time.Duration) (*ConnManager[*mockClient], *mockHandlerWithInsecure) {
+ t.Helper()
+ handler := &mockHandlerWithInsecure{}
+ mgr := NewConnManager(ConnManagerConfig[*mockClient]{
+ Handler: handler,
+ Logger: logger.GetLogger("test-hc"),
+ HealthCheckInterval: interval,
+ HealthCheckTimeout: 1 * time.Second,
+ })
+ t.Cleanup(func() { mgr.GracefulStop() })
+ return mgr, handler
+}
+
+func TestPeriodicHealthCheck(t *testing.T) {
+ t.Run("unhealthy_node_moves_to_evictable", func(t *testing.T) {
+ // Start a real gRPC health server, register a node, then stop the server.
+ // The periodic health check should detect the node is unhealthy
+ // and call OnInactive.
+ addr, stopServer := startHealthServer(t)
+
+ mgr, handler := newHealthCheckConnManager(t, 200*time.Millisecond)
+
+ // Register the node — health check passes, node becomes active.
+ mgr.OnAddOrUpdate(buildNode("hc-node", addr))
+ require.Equal(t, 1, mgr.ActiveCount())
+ require.True(t, handler.isActive("hc-node"))
+
+ // Stop the server to make the node unhealthy.
+ stopServer()
+
+ // Wait for periodic health check to detect the failure.
+ require.Eventually(t, func() bool {
+ return mgr.ActiveCount() == 0
+ }, testflags.EventuallyTimeout, 100*time.Millisecond,
+ "node should be moved from active to evictable after health check failure")
+
+ // Verify OnInactive was called.
+ assert.True(t, handler.isInactive("hc-node"))
+ assert.Equal(t, 1, mgr.EvictableCount())
+ })
+
+ t.Run("healthy_node_stays_active", func(t *testing.T) {
+ // Start a real gRPC health server that remains running.
+ // The periodic health check should keep the node active.
+ addr, stopServer := startHealthServer(t)
+ defer stopServer()
+
+ mgr, handler := newHealthCheckConnManager(t, 200*time.Millisecond)
+
+ mgr.OnAddOrUpdate(buildNode("hc-stable", addr))
+ require.Equal(t, 1, mgr.ActiveCount())
+
+ // Wait for several health check cycles.
+ time.Sleep(1 * time.Second)
+
+ // Node should still be active.
+ assert.Equal(t, 1, mgr.ActiveCount())
+ assert.Equal(t, 0, mgr.EvictableCount())
+ assert.True(t, handler.isActive("hc-stable"))
+ })
+
+ t.Run("node_recovers_after_server_restart", func(t *testing.T) {
+ // Start server, register node, stop server (node becomes evictable),
+ // restart server on same address (node should recover to active).
+ addr, stopServer := startHealthServer(t)
+
+ mgr, handler := newHealthCheckConnManager(t, 200*time.Millisecond)
+
+ mgr.OnAddOrUpdate(buildNode("hc-restart", addr))
+ require.Equal(t, 1, mgr.ActiveCount())
+
+ // Stop server — node should become evictable.
+ stopServer()
+ require.Eventually(t, func() bool {
+ return mgr.ActiveCount() == 0
+ }, testflags.EventuallyTimeout, 100*time.Millisecond,
+ "node should become evictable after server stop")
+ assert.True(t, handler.isInactive("hc-restart"))
+
+ // Restart server on the same address.
+ _, stopRestarted := startHealthServerOnAddr(t, addr)
+ defer stopRestarted()
+
+ // The eviction retry goroutine should reconnect the node.
+ require.Eventually(t, func() bool {
+ return mgr.ActiveCount() == 1
+ }, testflags.EventuallyTimeout, 100*time.Millisecond,
+ "node should recover to active after server restart")
+ assert.True(t, handler.isActive("hc-restart"))
+ })
+
+ t.Run("disabled_when_interval_is_zero", func(t *testing.T) {
+ // With HealthCheckInterval=0, no periodic health check goroutine should run.
+ // Start a real server but the health check should never fire.
+ addr, stopServer := startHealthServer(t)
+
+ mgr, _ := newHealthCheckConnManager(t, 0)
+ mgr.OnAddOrUpdate(buildNode("no-hc-node", addr))
+ require.Equal(t, 1, mgr.ActiveCount())
+
+ // Stop the server. Without periodic health check, the node stays active.
+ stopServer()
+ time.Sleep(500 * time.Millisecond)
+
+ // Node should still be active — no health check removed it.
+ assert.Equal(t, 1, mgr.ActiveCount())
+ })
+
+ t.Run("graceful_stop_terminates_health_check", func(t *testing.T) {
+ // Verify that GracefulStop cleanly shuts down the periodic health check goroutine.
+ addr, stopServer := startHealthServer(t)
+ defer stopServer()
+
+ handler := &mockHandlerWithInsecure{}
+ mgr := NewConnManager(ConnManagerConfig[*mockClient]{
+ Handler: handler,
+ Logger: logger.GetLogger("test-hc-stop"),
+ HealthCheckInterval: 100 * time.Millisecond,
+ HealthCheckTimeout: 1 * time.Second,
+ })
+
+ mgr.OnAddOrUpdate(buildNode("hc-stop", addr))
+ require.Equal(t, 1, mgr.ActiveCount())
+
+ // GracefulStop should return without hanging (health check goroutine exits).
+ done := make(chan struct{})
+ go func() {
+ mgr.GracefulStop()
+ close(done)
+ }()
+ select {
+ case <-done:
+ // Success — GracefulStop returned.
+ case <-time.After(5 * time.Second):
+ t.Fatal("GracefulStop did not return in time — periodic health check may be leaked")
+ }
+ })
+}
+
+func TestPeriodicHealthCheck_ConnectsViaGRPC(t *testing.T) {
+ // Integration test: verify that OnAddOrUpdate creates a real gRPC connection
+ // and the periodic health check keeps the node active end-to-end.
+ addr, stopServer := startHealthServer(t)
+
+ mgr, handler := newHealthCheckConnManager(t, 200*time.Millisecond)
+
+ // Register the node via ConnManager — should establish a real gRPC connection.
+ mgr.OnAddOrUpdate(buildNode("grpc-node", addr))
+ require.Equal(t, 1, mgr.ActiveCount())
+ require.True(t, handler.isActive("grpc-node"))
+
+ // Wait for several health check cycles — node should stay active.
+ time.Sleep(1 * time.Second)
+ assert.Equal(t, 1, mgr.ActiveCount())
+
+ // Stop server — periodic health check should detect failure and evict.
+ stopServer()
+ require.Eventually(t, func() bool {
+ return mgr.ActiveCount() == 0
+ }, testflags.EventuallyTimeout, 100*time.Millisecond,
+ "node should be evicted after server stops")
+ assert.True(t, handler.isInactive("grpc-node"))
+}
diff --git a/pkg/grpchelper/helpers_test.go b/pkg/grpchelper/helpers_test.go
index 1c6c9815c..5f62a8fc1 100644
--- a/pkg/grpchelper/helpers_test.go
+++ b/pkg/grpchelper/helpers_test.go
@@ -18,6 +18,8 @@
package grpchelper
import (
+ "sync"
+
"google.golang.org/grpc"
databasev1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/database/v1"
@@ -37,6 +39,7 @@ func (m *mockClient) Close() error {
type mockHandler struct {
activeNodes map[string]*mockClient
inactiveNodes map[string]*mockClient
+ mu sync.Mutex
}
func (h *mockHandler) AddressOf(node *databasev1.Node) string {
@@ -52,6 +55,8 @@ func (h *mockHandler) NewClient(_ *grpc.ClientConn, _ *databasev1.Node) (*mockCl
}
func (h *mockHandler) OnActive(name string, client *mockClient) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
if h.activeNodes == nil {
h.activeNodes = make(map[string]*mockClient)
}
@@ -59,9 +64,25 @@ func (h *mockHandler) OnActive(name string, client *mockClient) {
}
func (h *mockHandler) OnInactive(name string, client *mockClient) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
if h.inactiveNodes == nil {
h.inactiveNodes = make(map[string]*mockClient)
}
h.inactiveNodes[name] = client
delete(h.activeNodes, name)
}
+
+func (h *mockHandler) isActive(name string) bool {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ _, ok := h.activeNodes[name]
+ return ok
+}
+
+func (h *mockHandler) isInactive(name string) bool {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ _, ok := h.inactiveNodes[name]
+ return ok
+}
(skywalking-banyandb) 17/24: fix(stream): skip element-index visit when idx/ is absent (#1090)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 081464e30d6cfb0271639d8ef384a31a838cd4f6
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Sun Apr 26 18:58:56 2026 +0800
fix(stream): skip element-index visit when idx/ is absent (#1090)
---
banyand/stream/visitor.go | 17 ++++++++++++++---
banyand/stream/visitor_test.go | 34 ++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+), 3 deletions(-)
diff --git a/banyand/stream/visitor.go b/banyand/stream/visitor.go
index 19ab1d589..3000d5e62 100644
--- a/banyand/stream/visitor.go
+++ b/banyand/stream/visitor.go
@@ -18,6 +18,8 @@
package stream
import (
+ "fmt"
+ "os"
"path/filepath"
"strconv"
@@ -40,6 +42,7 @@ type Visitor interface {
// streamSegmentVisitor adapts Visitor to work with storage.SegmentVisitor.
type streamSegmentVisitor struct {
visitor Visitor
+ lfs fs.FileSystem
}
// VisitSeries implements storage.SegmentVisitor.
@@ -60,8 +63,7 @@ func (sv *streamSegmentVisitor) VisitShard(segmentTR *timestamp.TimeRange, shard
// visitShardParts visits all part directories within a shard.
func (sv *streamSegmentVisitor) visitShardParts(segmentTR *timestamp.TimeRange, shardID common.ShardID, shardPath string) error {
- lfs := fs.NewLocalFileSystem()
- entries := lfs.ReadDir(shardPath)
+ entries := sv.lfs.ReadDir(shardPath)
for _, entry := range entries {
if !entry.IsDir() {
@@ -91,6 +93,15 @@ func (sv *streamSegmentVisitor) visitShardParts(segmentTR *timestamp.TimeRange,
// visitShardElementIndex visits the element index directory within a shard.
func (sv *streamSegmentVisitor) visitShardElementIndex(segmentTR *timestamp.TimeRange, shardID common.ShardID, shardPath string) error {
indexPath := filepath.Join(shardPath, elementIndexFilename)
+ // A shard with parts but no indexed elements has no idx/ directory in the snapshot;
+ // skip instead of letting the visitor's ReadDir panic on the missing path.
+ // Surface other stat errors (permission, I/O) so real filesystem problems aren't hidden.
+ if _, err := os.Stat(indexPath); err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return fmt.Errorf("failed to stat element index directory %q: %w", indexPath, err)
+ }
return sv.visitor.VisitElementIndex(segmentTR, shardID, indexPath)
}
@@ -99,6 +110,6 @@ func (sv *streamSegmentVisitor) visitShardElementIndex(segmentTR *timestamp.Time
// This function works directly with the filesystem without requiring a database instance.
// Returns a list of segment suffixes that were visited.
func VisitStreamsInTimeRange(tsdbRootPath string, timeRange timestamp.TimeRange, visitor Visitor, segmentInterval storage.IntervalRule) ([]string, error) {
- adapter := &streamSegmentVisitor{visitor: visitor}
+ adapter := &streamSegmentVisitor{visitor: visitor, lfs: fs.NewLocalFileSystem()}
return storage.VisitSegmentsInTimeRange(tsdbRootPath, timeRange, adapter, segmentInterval)
}
diff --git a/banyand/stream/visitor_test.go b/banyand/stream/visitor_test.go
index 687ac69f8..c2bd1bb4b 100644
--- a/banyand/stream/visitor_test.go
+++ b/banyand/stream/visitor_test.go
@@ -120,3 +120,37 @@ func TestVisitStreamsInTimeRange(t *testing.T) {
assert.Equal(t, common.ShardID(0), visitor.visitedElementIndex[0].shardID)
assert.Contains(t, visitor.visitedElementIndex[0].path, elementIndexFilename)
}
+
+func TestVisitStreamsInTimeRange_MissingElementIndexDir(t *testing.T) {
+ tmpDir, err := os.MkdirTemp("", "stream_visitor_missing_idx_test")
+ require.NoError(t, err)
+ defer os.RemoveAll(tmpDir)
+
+ now := time.Now()
+ segmentTime := now.Format("2006010215")
+ segmentDir := filepath.Join(tmpDir, "seg-"+segmentTime)
+ require.NoError(t, os.MkdirAll(segmentDir, 0o755))
+
+ seriesDir := filepath.Join(segmentDir, "sidx")
+ require.NoError(t, os.MkdirAll(seriesDir, 0o755))
+
+ shardDir := filepath.Join(segmentDir, "shard-1")
+ require.NoError(t, os.MkdirAll(shardDir, 0o755))
+
+ partDir := filepath.Join(shardDir, "000000000000001a")
+ require.NoError(t, os.MkdirAll(partDir, 0o755))
+
+ visitor := &TestVisitor{}
+ timeRange := timestamp.TimeRange{
+ Start: now.Add(-time.Hour),
+ End: now.Add(time.Hour),
+ }
+ intervalRule := storage.IntervalRule{Unit: storage.HOUR, Num: 1}
+
+ suffixes, err := VisitStreamsInTimeRange(tmpDir, timeRange, visitor, intervalRule)
+ require.NoError(t, err)
+ assert.NotEmpty(t, suffixes)
+
+ assert.Len(t, visitor.visitedParts, 1)
+ assert.Empty(t, visitor.visitedElementIndex, "VisitElementIndex must be skipped when idx/ is absent")
+}
(skywalking-banyandb) 06/24: chore(deps): upgrade Go and npm dependencies (#1058)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit cd3ac57e3b329ddd7fc5869a31fe26a4cd514e11
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Fri Apr 10 13:24:42 2026 +0800
chore(deps): upgrade Go and npm dependencies (#1058)
Upgrade etcd to v3.6.10, OpenTelemetry to v1.43.0, AWS SDK to v1.41.5,
Google Cloud libraries, and npm packages in ui and mcp modules.
---
dist/LICENSE | 162 +++++-----
dist/licenses/ui-licenses/license-postcss.txt | 2 +-
go.mod | 116 +++----
go.sum | 264 ++++++++--------
mcp/LICENSE | 18 +-
mcp/package-lock.json | 438 +++++++++++++-------------
ui/LICENSE | 28 +-
ui/package-lock.json | 352 ++++++++++-----------
8 files changed, 686 insertions(+), 694 deletions(-)
diff --git a/dist/LICENSE b/dist/LICENSE
index cf0b6aa70..25bdabdce 100644
--- a/dist/LICENSE
+++ b/dist/LICENSE
@@ -180,38 +180,38 @@ Apache-2.0 licenses
cel.dev/expr v0.25.1 Apache-2.0
cloud.google.com/go v0.123.0 Apache-2.0
- cloud.google.com/go/auth v0.18.2 Apache-2.0
+ cloud.google.com/go/auth v0.20.0 Apache-2.0
cloud.google.com/go/auth/oauth2adapt v0.2.8 Apache-2.0
cloud.google.com/go/compute/metadata v0.9.0 Apache-2.0
- cloud.google.com/go/iam v1.5.3 Apache-2.0
- cloud.google.com/go/monitoring v1.24.3 Apache-2.0
- cloud.google.com/go/storage v1.61.3 Apache-2.0
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 Apache-2.0
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 Apache-2.0
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 Apache-2.0
+ cloud.google.com/go/iam v1.7.0 Apache-2.0
+ cloud.google.com/go/monitoring v1.25.0 Apache-2.0
+ cloud.google.com/go/storage v1.62.0 Apache-2.0
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 Apache-2.0
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 Apache-2.0
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 Apache-2.0
github.com/SkyAPM/bluge v0.0.0-20250804100126-cccf29a55f01 Apache-2.0
github.com/SkyAPM/ice v0.0.0-20250619023539-b5173603b0b3 Apache-2.0
github.com/SkyAPM/ktm-ebpf v0.0.0-20260228024820-81a19d950bff Apache-2.0
github.com/apache/skywalking-cli v0.0.0-20240227151024-ee371a210afe Apache-2.0
- github.com/aws/aws-sdk-go-v2 v1.41.4 Apache-2.0
- github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 Apache-2.0
- github.com/aws/aws-sdk-go-v2/config v1.32.12 Apache-2.0
- github.com/aws/aws-sdk-go-v2/credentials v1.19.12 Apache-2.0
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 Apache-2.0
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 Apache-2.0
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 Apache-2.0
+ github.com/aws/aws-sdk-go-v2 v1.41.5 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/config v1.32.14 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.14 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 Apache-2.0
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 Apache-2.0
- github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 Apache-2.0
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 Apache-2.0
- github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 Apache-2.0
- github.com/aws/smithy-go v1.24.2 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 Apache-2.0
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 Apache-2.0
+ github.com/aws/smithy-go v1.24.3 Apache-2.0
github.com/blevesearch/segment v0.9.1 Apache-2.0
github.com/blevesearch/vellum v1.2.0 Apache-2.0
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 Apache-2.0
@@ -221,17 +221,17 @@ Apache-2.0 licenses
github.com/coreos/go-semver v0.3.1 Apache-2.0
github.com/coreos/go-systemd/v22 v22.7.0 Apache-2.0
github.com/distribution/reference v0.6.0 Apache-2.0
- github.com/docker/cli v29.3.0+incompatible Apache-2.0
+ github.com/docker/cli v29.4.0+incompatible Apache-2.0
github.com/docker/go-connections v0.6.0 Apache-2.0
github.com/docker/go-units v0.5.0 Apache-2.0
github.com/envoyproxy/go-control-plane/envoy v1.37.0 Apache-2.0
github.com/envoyproxy/protoc-gen-validate v1.3.3 Apache-2.0
github.com/go-ini/ini v1.67.0 Apache-2.0
- github.com/go-jose/go-jose/v4 v4.1.3 Apache-2.0
+ github.com/go-jose/go-jose/v4 v4.1.4 Apache-2.0
github.com/go-logr/logr v1.4.3 Apache-2.0
github.com/go-logr/stdr v1.2.2 Apache-2.0
github.com/google/btree v1.1.3 Apache-2.0
- github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc Apache-2.0
+ github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 Apache-2.0
github.com/google/s2a-go v0.1.9 Apache-2.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 Apache-2.0
github.com/googleapis/enterprise-certificate-proxy v0.3.14 Apache-2.0
@@ -242,10 +242,10 @@ Apache-2.0 licenses
github.com/machinebox/graphql v0.2.2 Apache-2.0
github.com/minio/crc64nvme v1.1.1 Apache-2.0
github.com/minio/md5-simd v1.1.2 Apache-2.0
- github.com/minio/minio-go/v7 v7.0.99 Apache-2.0
+ github.com/minio/minio-go/v7 v7.0.100 Apache-2.0
github.com/moby/docker-image-spec v1.3.1 Apache-2.0
- github.com/moby/moby/api v1.54.0 Apache-2.0
- github.com/moby/moby/client v0.3.0 Apache-2.0
+ github.com/moby/moby/api v1.54.1 Apache-2.0
+ github.com/moby/moby/client v0.4.0 Apache-2.0
github.com/moby/sys/user v0.4.0 Apache-2.0
github.com/moby/term v0.5.2 Apache-2.0
github.com/mschoch/smat v0.2.0 Apache-2.0
@@ -267,20 +267,20 @@ Apache-2.0 licenses
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 Apache-2.0
github.com/xeipuuv/gojsonschema v1.2.0 Apache-2.0
github.com/zinclabs/bluge_segment_api v1.0.0 Apache-2.0
- go.etcd.io/etcd/api/v3 v3.6.9 Apache-2.0
- go.etcd.io/etcd/client/pkg/v3 v3.6.9 Apache-2.0
- go.etcd.io/etcd/client/v3 v3.6.9 Apache-2.0
- go.etcd.io/etcd/pkg/v3 v3.6.9 Apache-2.0
- go.etcd.io/etcd/server/v3 v3.6.9 Apache-2.0
+ go.etcd.io/etcd/api/v3 v3.6.10 Apache-2.0
+ go.etcd.io/etcd/client/pkg/v3 v3.6.10 Apache-2.0
+ go.etcd.io/etcd/client/v3 v3.6.10 Apache-2.0
+ go.etcd.io/etcd/pkg/v3 v3.6.10 Apache-2.0
+ go.etcd.io/etcd/server/v3 v3.6.10 Apache-2.0
go.etcd.io/raft/v3 v3.6.0 Apache-2.0
go.opentelemetry.io/auto/sdk v1.2.1 Apache-2.0
go.opentelemetry.io/proto/otlp v1.10.0 Apache-2.0
go.uber.org/mock v0.6.0 Apache-2.0
go.yaml.in/yaml/v2 v2.4.4 Apache-2.0
- google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 Apache-2.0
- google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 Apache-2.0
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 Apache-2.0
- google.golang.org/grpc v1.79.3 Apache-2.0
+ google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d Apache-2.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d Apache-2.0
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d Apache-2.0
+ google.golang.org/grpc v1.80.0 Apache-2.0
skywalking.apache.org/repo/goapi v0.0.0-20251021000546-17778a1a5d70 Apache-2.0
========================================================================
@@ -288,16 +288,16 @@ Apache-2.0 and BSD-3-Clause licenses
========================================================================
github.com/RoaringBitmap/roaring v1.9.4 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/contrib/detectors/gcp v1.42.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/otel v1.42.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/otel/metric v1.42.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/otel/sdk v1.42.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/otel/sdk/metric v1.42.0 Apache-2.0 and BSD-3-Clause
- go.opentelemetry.io/otel/trace v1.42.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/contrib/detectors/gcp v1.43.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/otel v1.43.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/otel/metric v1.43.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/otel/sdk v1.43.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/otel/sdk/metric v1.43.0 Apache-2.0 and BSD-3-Clause
+ go.opentelemetry.io/otel/trace v1.43.0 Apache-2.0 and BSD-3-Clause
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 Apache-2.0 and BSD-3-Clause
========================================================================
@@ -335,10 +335,10 @@ BSD-3-Clause licenses
github.com/golang/snappy v1.0.0 BSD-3-Clause
github.com/google/go-cmp v0.7.0 BSD-3-Clause
github.com/google/uuid v1.6.0 BSD-3-Clause
- github.com/googleapis/gax-go/v2 v2.19.0 BSD-3-Clause
+ github.com/googleapis/gax-go/v2 v2.21.0 BSD-3-Clause
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 BSD-3-Clause
github.com/klauspost/crc32 v1.3.0 BSD-3-Clause
- github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 BSD-3-Clause
+ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e BSD-3-Clause
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 BSD-3-Clause
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 BSD-3-Clause
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 BSD-3-Clause
@@ -352,11 +352,11 @@ BSD-3-Clause licenses
golang.org/x/net v0.52.0 BSD-3-Clause
golang.org/x/oauth2 v0.36.0 BSD-3-Clause
golang.org/x/sync v0.20.0 BSD-3-Clause
- golang.org/x/sys v0.42.0 BSD-3-Clause
+ golang.org/x/sys v0.43.0 BSD-3-Clause
golang.org/x/text v0.35.0 BSD-3-Clause
golang.org/x/time v0.15.0 BSD-3-Clause
golang.org/x/tools v0.43.0 BSD-3-Clause
- google.golang.org/api v0.272.0 BSD-3-Clause
+ google.golang.org/api v0.275.0 BSD-3-Clause
google.golang.org/protobuf v1.36.11 BSD-3-Clause
========================================================================
@@ -376,7 +376,7 @@ MIT licenses
========================================================================
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 MIT
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 MIT
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 MIT
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 MIT
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c MIT
github.com/Masterminds/semver/v3 v3.4.0 MIT
@@ -404,16 +404,16 @@ MIT licenses
github.com/golang-jwt/jwt/v5 v5.3.1 MIT
github.com/klauspost/cpuid/v2 v2.3.0 MIT
github.com/mattn/go-colorable v0.1.14 MIT
- github.com/mattn/go-isatty v0.0.20 MIT
- github.com/montanaflynn/stats v0.8.2 MIT
+ github.com/mattn/go-isatty v0.0.21 MIT
+ github.com/montanaflynn/stats v0.9.0 MIT
github.com/onsi/ginkgo/v2 v2.28.1 MIT
github.com/onsi/gomega v1.39.1 MIT
- github.com/pelletier/go-toml/v2 v2.2.4 MIT
+ github.com/pelletier/go-toml/v2 v2.3.0 MIT
github.com/philhofer/fwd v1.2.0 MIT
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 MIT
github.com/robfig/cron/v3 v3.0.1 MIT
github.com/rs/xid v1.6.0 MIT
- github.com/rs/zerolog v1.34.0 MIT
+ github.com/rs/zerolog v1.35.0 MIT
github.com/sagikazarmark/locafero v0.12.0 MIT
github.com/sirupsen/logrus v1.9.4 MIT
github.com/spf13/cast v1.10.0 MIT
@@ -529,21 +529,21 @@ MIT licenses
@parcel/watcher-linux-x64-musl 2.5.6 MIT
@popperjs/core 2.11.8 MIT
@tanstack/match-sorter-utils 8.19.4 MIT
- @tanstack/query-core 5.95.2 MIT
- @tanstack/vue-query 5.95.2 MIT
+ @tanstack/query-core 5.97.0 MIT
+ @tanstack/vue-query 5.97.0 MIT
@types/lodash 4.17.24 MIT
@types/lodash-es 4.17.12 MIT
@types/web-bluetooth 0.0.20 MIT
- @vue/compiler-core 3.5.31 MIT
- @vue/compiler-dom 3.5.31 MIT
- @vue/compiler-sfc 3.5.31 MIT
- @vue/compiler-ssr 3.5.31 MIT
+ @vue/compiler-core 3.5.32 MIT
+ @vue/compiler-dom 3.5.32 MIT
+ @vue/compiler-sfc 3.5.32 MIT
+ @vue/compiler-ssr 3.5.32 MIT
@vue/devtools-api 6.6.4 MIT
- @vue/reactivity 3.5.31 MIT
- @vue/runtime-core 3.5.31 MIT
- @vue/runtime-dom 3.5.31 MIT
- @vue/server-renderer 3.5.31 MIT
- @vue/shared 3.5.31 MIT
+ @vue/reactivity 3.5.32 MIT
+ @vue/runtime-core 3.5.32 MIT
+ @vue/runtime-dom 3.5.32 MIT
+ @vue/server-renderer 3.5.32 MIT
+ @vue/shared 3.5.32 MIT
@vueuse/core 12.0.0 MIT
@vueuse/metadata 12.0.0 MIT
@vueuse/shared 12.0.0 MIT
@@ -573,18 +573,18 @@ MIT licenses
node-addon-api 7.1.1 MIT
picomatch 4.0.4 MIT
pinia 2.3.1 MIT
- postcss 8.5.8 MIT
+ postcss 8.5.9 MIT
process-nextick-args 2.0.1 MIT
readable-stream 2.3.8 MIT
readdirp 4.1.2 MIT
remove-accents 0.5.0 MIT
safe-buffer 5.1.2 MIT
- sass 1.98.0 MIT
+ sass 1.99.0 MIT
setimmediate 1.0.5 MIT
string_decoder 1.1.1 MIT
universalify 2.0.1 MIT
util-deprecate 1.0.2 MIT
- vue 3.5.31 MIT
+ vue 3.5.32 MIT
vue-component-type-helpers 3.2.6 MIT
vue-demi 0.14.10 MIT
vue-router 4.6.4 MIT
@@ -632,7 +632,7 @@ BSD-3-Clause licenses
========================================================================
fast-uri 3.1.0 BSD-3-Clause
- qs 6.15.0 BSD-3-Clause
+ qs 6.15.1 BSD-3-Clause
========================================================================
ISC licenses
@@ -644,14 +644,14 @@ ISC licenses
setprototypeof 1.2.0 ISC
which 2.0.2 ISC
wrappy 1.0.2 ISC
- zod-to-json-schema 3.25.1 ISC
+ zod-to-json-schema 3.25.2 ISC
========================================================================
MIT licenses
========================================================================
- @hono/node-server 1.19.11 MIT
- @modelcontextprotocol/sdk 1.28.0 MIT
+ @hono/node-server 1.19.13 MIT
+ @modelcontextprotocol/sdk 1.29.0 MIT
accepts 2.0.0 MIT
ajv 8.18.0 MIT
ajv-formats 3.0.1 MIT
@@ -659,7 +659,7 @@ MIT licenses
bytes 3.1.2 MIT
call-bind-apply-helpers 1.0.2 MIT
call-bound 1.0.4 MIT
- content-disposition 1.0.1 MIT
+ content-disposition 1.1.0 MIT
content-type 1.0.5 MIT
cookie 0.7.2 MIT
cookie-signature 1.2.2 MIT
@@ -678,7 +678,7 @@ MIT licenses
eventsource 3.0.7 MIT
eventsource-parser 3.0.6 MIT
express 5.2.1 MIT
- express-rate-limit 8.3.1 MIT
+ express-rate-limit 8.3.2 MIT
fast-deep-equal 3.1.3 MIT
finalhandler 2.1.1 MIT
forwarded 0.2.0 MIT
@@ -689,7 +689,7 @@ MIT licenses
gopd 1.2.0 MIT
has-symbols 1.1.0 MIT
hasown 2.0.2 MIT
- hono 4.12.9 MIT
+ hono 4.12.12 MIT
http-errors 2.0.1 MIT
iconv-lite 0.7.2 MIT
ip-address 10.1.0 MIT
@@ -709,7 +709,7 @@ MIT licenses
on-finished 2.4.1 MIT
parseurl 1.3.3 MIT
path-key 3.1.1 MIT
- path-to-regexp 8.4.1 MIT
+ path-to-regexp 8.4.2 MIT
pkce-challenge 5.0.1 MIT
proxy-addr 2.0.7 MIT
range-parser 1.2.1 MIT
@@ -722,7 +722,7 @@ MIT licenses
shebang-command 2.0.0 MIT
shebang-regex 3.0.0 MIT
side-channel 1.1.0 MIT
- side-channel-list 1.0.0 MIT
+ side-channel-list 1.0.1 MIT
side-channel-map 1.0.1 MIT
side-channel-weakmap 1.0.2 MIT
statuses 2.0.2 MIT
diff --git a/dist/licenses/ui-licenses/license-postcss.txt b/dist/licenses/ui-licenses/license-postcss.txt
index da057b456..c2314d53d 100644
--- a/dist/licenses/ui-licenses/license-postcss.txt
+++ b/dist/licenses/ui-licenses/license-postcss.txt
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright 2013 Andrey Sitnik <an...@sitnik.ru>
+Copyright 2013 Andrey Sitnik <an...@sitnik.es>
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
diff --git a/go.mod b/go.mod
index 82ffca5c6..b104d51c8 100644
--- a/go.mod
+++ b/go.mod
@@ -3,16 +3,16 @@ module github.com/apache/skywalking-banyandb
go 1.25.0
require (
- cloud.google.com/go/storage v1.61.3
+ cloud.google.com/go/storage v1.62.0
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/RoaringBitmap/roaring v1.9.4
github.com/SkyAPM/ktm-ebpf v0.0.0-20260228024820-81a19d950bff
github.com/alecthomas/participle/v2 v2.1.4
github.com/apache/skywalking-cli v0.0.0-20240227151024-ee371a210afe
- github.com/aws/aws-sdk-go-v2 v1.41.4
- github.com/aws/aws-sdk-go-v2/config v1.32.12
- github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1
+ github.com/aws/aws-sdk-go-v2 v1.41.5
+ github.com/aws/aws-sdk-go-v2/config v1.32.14
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0
github.com/benbjohnson/clock v1.3.5
github.com/blugelabs/bluge v0.2.2
github.com/cespare/xxhash/v2 v2.3.0
@@ -26,15 +26,15 @@ require (
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0
github.com/hashicorp/golang-lru v1.0.2
- github.com/minio/minio-go/v7 v7.0.99
- github.com/montanaflynn/stats v0.8.2
+ github.com/minio/minio-go/v7 v7.0.100
+ github.com/montanaflynn/stats v0.9.0
github.com/oklog/run v1.2.0
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1
github.com/ory/dockertest/v3 v3.12.0
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
- github.com/rs/zerolog v1.34.0
+ github.com/rs/zerolog v1.35.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
@@ -42,18 +42,18 @@ require (
github.com/urfave/cli/v2 v2.27.7
github.com/xhit/go-str2duration/v2 v2.1.0
github.com/zenizh/go-capturer v0.0.0-20211219060012-52ea6c8fed04
- go.etcd.io/etcd/client/v3 v3.6.9
- go.etcd.io/etcd/server/v3 v3.6.9
+ go.etcd.io/etcd/client/v3 v3.6.10
+ go.etcd.io/etcd/server/v3 v3.6.10
go.uber.org/automaxprocs v1.6.0
go.uber.org/mock v0.6.0
go.uber.org/multierr v1.11.0
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90
golang.org/x/mod v0.34.0
golang.org/x/oauth2 v0.36.0
- google.golang.org/api v0.272.0
- google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7
- google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7
- google.golang.org/grpc v1.79.3
+ google.golang.org/api v0.275.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d
+ google.golang.org/grpc v1.80.0
google.golang.org/protobuf v1.36.11
sigs.k8s.io/yaml v1.6.0
skywalking.apache.org/repo/goapi v0.0.0-20251021000546-17778a1a5d70
@@ -62,37 +62,37 @@ require (
require (
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.123.0 // indirect
- cloud.google.com/go/auth v0.18.2 // indirect
+ cloud.google.com/go/auth v0.20.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
- cloud.google.com/go/iam v1.5.3 // indirect
- cloud.google.com/go/monitoring v1.24.3 // indirect
+ cloud.google.com/go/iam v1.7.0 // indirect
+ cloud.google.com/go/monitoring v1.25.0 // indirect
dario.cat/mergo v1.0.2 // indirect
- github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
+ github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
- github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 // indirect
+ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/VictoriaMetrics/fastcache v1.13.3 // indirect
- github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 // indirect
- github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect
- github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect
- github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect
- github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 // indirect
+ github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
- github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect
- github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 // indirect
- github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect
- github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect
- github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect
- github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect
- github.com/aws/smithy-go v1.24.2 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 // indirect
+ github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
+ github.com/aws/smithy-go v1.24.3 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect
github.com/containerd/continuity v0.4.5 // indirect
@@ -100,13 +100,13 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/distribution/reference v0.6.0 // indirect
- github.com/docker/cli v29.3.0+incompatible // indirect
+ github.com/docker/cli v29.4.0+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-ini/ini v1.67.0 // indirect
- github.com/go-jose/go-jose/v4 v4.1.3 // indirect
+ github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
@@ -114,7 +114,7 @@ require (
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect
- github.com/googleapis/gax-go/v2 v2.19.0 // indirect
+ github.com/googleapis/gax-go/v2 v2.21.0 // indirect
github.com/kamstrup/intmap v0.5.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
@@ -122,8 +122,8 @@ require (
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
- github.com/moby/moby/api v1.54.0 // indirect
- github.com/moby/moby/client v0.3.0 // indirect
+ github.com/moby/moby/api v1.54.1 // indirect
+ github.com/moby/moby/client v0.4.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
@@ -142,9 +142,9 @@ require (
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
go.etcd.io/raft/v3 v3.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
- go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
- go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect
+ go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
+ go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/sync v0.20.0 // indirect
@@ -176,17 +176,17 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/btree v1.1.3 // indirect
- github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
+ github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/klauspost/compress v1.18.5
- github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 // indirect
+ github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
- github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mschoch/smat v0.2.0 // indirect
- github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/pelletier/go-toml/v2 v2.3.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
@@ -207,25 +207,25 @@ require (
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.etcd.io/bbolt v1.4.3 // indirect
- go.etcd.io/etcd/api/v3 v3.6.9
- go.etcd.io/etcd/client/pkg/v3 v3.6.9
- go.etcd.io/etcd/pkg/v3 v3.6.9 // indirect
- go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
- go.opentelemetry.io/otel v1.42.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect
- go.opentelemetry.io/otel/metric v1.42.0 // indirect
- go.opentelemetry.io/otel/sdk v1.42.0 // indirect
- go.opentelemetry.io/otel/trace v1.42.0 // indirect
+ go.etcd.io/etcd/api/v3 v3.6.10
+ go.etcd.io/etcd/client/pkg/v3 v3.6.10
+ go.etcd.io/etcd/pkg/v3 v3.6.10 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
+ go.opentelemetry.io/otel v1.43.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
+ go.opentelemetry.io/otel/metric v1.43.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.43.0 // indirect
+ go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/zap v1.27.1
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.52.0 // indirect
- golang.org/x/sys v0.42.0
+ golang.org/x/sys v0.43.0
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.43.0 // indirect
- google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
+ google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1
)
diff --git a/go.sum b/go.sum
index a838715fb..f10f100ec 100644
--- a/go.sum
+++ b/go.sum
@@ -2,24 +2,24 @@ cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
-cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
-cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
+cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
+cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
-cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=
-cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=
-cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA=
-cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak=
-cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8=
-cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk=
-cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
-cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
-cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg=
-cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk=
-cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
-cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
+cloud.google.com/go/iam v1.7.0 h1:JD3zh0C6LHl16aCn5Akff0+GELdp1+4hmh6ndoFLl8U=
+cloud.google.com/go/iam v1.7.0/go.mod h1:tetWZW1PD/m6vcuY2Zj/aU0eCHNPuxedbnbRTyKXvdY=
+cloud.google.com/go/logging v1.14.0 h1:xpPpY8cVT6n9DgIRgrWyE+YEsGlO/994pWnbc7o5Eh4=
+cloud.google.com/go/logging v1.14.0/go.mod h1:jmI+Try/fZeOTOAer3wVYOuPf9WX9PyzhlSDoBAi4HM=
+cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY=
+cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
+cloud.google.com/go/monitoring v1.25.0 h1:HnsTIOxTN6BCSkt1P/Im23r1m7MHTTpmSYCzPkW7NK4=
+cloud.google.com/go/monitoring v1.25.0/go.mod h1:wlj6rX+JGyusw/8+2duW4cJ6kmDHGmde3zMTJuG3Jpc=
+cloud.google.com/go/storage v1.62.0 h1:w2pQJhpUqVerMON45vatE2FpCYsNTf7OHjkn6ux5mMU=
+cloud.google.com/go/storage v1.62.0/go.mod h1:T5hz3qzcpnxZ5LdKc7y8Tw7lh4v9zeeVyrD/cLJAzZU=
+cloud.google.com/go/trace v1.12.0 h1:XvWHYfr9q88cX4pZyou6qCcSagnuASyUq2ej1dB6NzQ=
+cloud.google.com/go/trace v1.12.0/go.mod h1:TOYfyeoyCGsSH0ifXD6Aius24uQI9xV3RyvOdljFIyg=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
@@ -28,8 +28,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEB
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
-github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4=
+github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
@@ -38,14 +38,14 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
-github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0/go.mod h1:hEpiGU18xf70qb3jbTcIggWAiEfX/cOIVc2OTe4OegA=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0 h1:ZIT85vKP7LBS84XJ0WdJ3dPOX3iz4j3c0+lpajGQMyo=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0/go.mod h1:rqP9UEhOXv9WhQ7Gjz+G5y/pf8+BJZW5/Ts0AhE0PwE=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 h1:0YP0+/ixwu+Uqeu/FGiBZNQ19huiUxxiPXIc9WsLKuQ=
+github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0/go.mod h1:6ZZMQhZKDvUvkJw2rc+oDP90tMMzuU/J+5HG1ZmPOmE=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
@@ -73,44 +73,44 @@ github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/apache/skywalking-cli v0.0.0-20240227151024-ee371a210afe h1:zIc2yfpc/vMpfTtWprCVpca6CMJwb6X9cknqAoFeEFo=
github.com/apache/skywalking-cli v0.0.0-20240227151024-ee371a210afe/go.mod h1:pu6Q19Xs38FSfy/IwnJGAMilO+W58/ugM8aMfLzw+i0=
-github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k=
-github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7 h1:3kGOqnh1pPeddVa/E37XNTaWJ8W6vrbYV9lJEkCnhuY=
-github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.7/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
-github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0=
-github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8=
-github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc=
+github.com/aws/aws-sdk-go-v2 v1.41.5 h1:dj5kopbwUsVUVFgO4Fi5BIT3t4WyqIDjGKCangnV/yY=
+github.com/aws/aws-sdk-go-v2 v1.41.5/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8 h1:eBMB84YGghSocM7PsjmmPffTa+1FBUeNvGvFou6V/4o=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.8/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
+github.com/aws/aws-sdk-go-v2/config v1.32.14 h1:opVIRo/ZbbI8OIqSOKmpFaY7IwfFUOCCXBsUpJOwDdI=
+github.com/aws/aws-sdk-go-v2/config v1.32.14/go.mod h1:U4/V0uKxh0Tl5sxmCBZ3AecYny4UNlVmObYjKuuaiOo=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.14 h1:n+UcGWAIZHkXzYt87uMFBv/l8THYELoX6gVcUvgl6fI=
+github.com/aws/aws-sdk-go-v2/credentials v1.19.14/go.mod h1:cJKuyWB59Mqi0jM3nFYQRmnHVQIcgoxjEMAbLkpr62w=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 h1:NUS3K4BTDArQqNu2ih7yeDLaS3bmHD0YndtA6UP884g=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21/go.mod h1:YWNWJQNjKigKY1RHVJCuupeWDrrHjRqHm0N9rdrWzYI=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 h1:Rgg6wvjjtX8bNHcvi9OnXWwcE0a2vGpbwmtICOsvcf4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21/go.mod h1:A/kJFst/nm//cyqonihbdpQZwiUhhzpqTsdbhDdRF9c=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 h1:PEgGVtPoB6NTpPrBgqSE5hE/o47Ij9qk/SEZFbUOe9A=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21/go.mod h1:p+hz+PRAYlY3zcpJhPwXlLC4C+kqn70WIHwnzAfs6ps=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21 h1:SwGMTMLIlvDNyhMteQ6r8IJSBPlRdXX5d4idhIGbkXA=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.21/go.mod h1:UUxgWxofmOdAMuqEsSppbDtGKLfR04HGsD0HXzvhI1k=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22 h1:rWyie/PxDRIdhNf4DzRk0lvjVOqFJuNnO8WwaIRVxzQ=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.22/go.mod h1:zd/JsJ4P7oGfUhXn1VyLqaRZwPmZwg44Jf2dS84Dm3Y=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12 h1:qtJZ70afD3ISKWnoX3xB0J2otEqu3LqicRcDBqsj0hQ=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.12/go.mod h1:v2pNpJbRNl4vEUWEh5ytQok0zACAKfdmKS51Hotc3pQ=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20 h1:siU1A6xjUZ2N8zjTHSXFhB9L/2OY8Dqs0xXiLjF30jA=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.20/go.mod h1:4TLZCmVJDM3FOu5P5TJP0zOlu9zWgDWU7aUxWbr+rcw=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1 h1:csi9NLpFZXb9fxY7rS1xVzgPRGMt7MSNWeQ6eo247kE=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.97.1/go.mod h1:qXVal5H0ChqXP63t6jze5LmFalc7+ZE7wOdLtZ0LCP0=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow=
-github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o=
-github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU=
-github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk=
-github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
-github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13 h1:JRaIgADQS/U6uXDqlPiefP32yXTda7Kqfx+LgspooZM=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.13/go.mod h1:CEuVn5WqOMilYl+tbccq8+N2ieCy0gVn3OtRb0vBNNM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 h1:c31//R3xgIJMSC8S6hEVq+38DcvUlgFY0FM6mSI5oto=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21/go.mod h1:r6+pf23ouCB718FUxaqzZdbpYFyDtehyZcmP5KL9FkA=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21 h1:ZlvrNcHSFFWURB8avufQq9gFsheUgjVD9536obIknfM=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.21/go.mod h1:cv3TNhVrssKR0O/xxLJVRfd2oazSnZnkUeTf6ctUwfQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0 h1:hlSuz394kV0vhv9drL5lhuEFbEOEP1VyQpy15qWh1Pk=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.99.0/go.mod h1:uoA43SdFwacedBfSgfFSjjCvYe8aYBS7EnU5GZ/YKMM=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 h1:QKZH0S178gCmFEgst8hN0mCX1KxLgHBKKY/CLqwP8lg=
+github.com/aws/aws-sdk-go-v2/service/signin v1.0.9/go.mod h1:7yuQJoT+OoH8aqIxw9vwF+8KpvLZ8AWmvmUWHsGQZvI=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 h1:lFd1+ZSEYJZYvv9d6kXzhkZu07si3f+GQ1AaYwa2LUM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.30.15/go.mod h1:WSvS1NLr7JaPunCXqpJnWk1Bjo7IxzZXrZi1QQCkuqM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 h1:dzztQ1YmfPrxdrOiuZRMF6fuOwWlWpD2StNLTceKpys=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19/go.mod h1:YO8TrYtFdl5w/4vmjL8zaBSsiNp3w0L1FfKVKenZT7w=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 h1:p8ogvvLugcR/zLBXTXrTkj0RYBUdErbMnAFFp12Lm/U=
+github.com/aws/aws-sdk-go-v2/service/sts v1.41.10/go.mod h1:60dv0eZJfeVXfbT1tFJinbHrDfSJ2GZl4Q//OSSNAVw=
+github.com/aws/smithy-go v1.24.3 h1:XgOAaUgx+HhVBoP4v8n6HCQoTRDhoMghKqw4LNHsDNg=
+github.com/aws/smithy-go v1.24.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/axiomhq/hyperloglog v0.2.6 h1:sRhvvF3RIXWQgAXaTphLp4yJiX4S0IN3MWTaAgZoRJw=
github.com/axiomhq/hyperloglog v0.2.6/go.mod h1:YjX/dQqCR/7QYX0g8mu8UZAjpIenz1FKM71UEsjFoTo=
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
@@ -153,7 +153,6 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
-github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -169,8 +168,8 @@ github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mT
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk=
-github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM=
+github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
@@ -203,8 +202,8 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
-github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
-github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
+github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -225,7 +224,6 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
-github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -240,8 +238,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
-github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
-github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
+github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg=
+github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@@ -250,8 +248,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
-github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE=
-github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA=
+github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI=
+github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -296,21 +294,18 @@ github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353 h1:X/79QL0b4YJVO5+O
github.com/leesper/go_rng v0.0.0-20190531154944-a612b043e353/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88 h1:PTw+yKnXcOFCR6+8hHTyWBeQ/P4Nb7dd4/0ohEcWQuM=
-github.com/lufia/plan9stats v0.0.0-20260216142805-b3301c5f2a88/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
+github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
+github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo=
github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
-github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
+github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
@@ -321,20 +316,20 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
-github.com/minio/minio-go/v7 v7.0.99 h1:2vH/byrwUkIpFQFOilvTfaUpvAX3fEFhEzO+DR3DlCE=
-github.com/minio/minio-go/v7 v7.0.99/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
+github.com/minio/minio-go/v7 v7.0.100 h1:ShkWi8Tyj9RtU57OQB2HIXKz4bFgtVib0bbT1sbtLI8=
+github.com/minio/minio-go/v7 v7.0.100/go.mod h1:EtGNKtlX20iL2yaYnxEigaIvj0G0GwSDnifnG8ClIdw=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
-github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0=
-github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
-github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs=
-github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ=
+github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4=
+github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
+github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw=
+github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g=
github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
-github.com/montanaflynn/stats v0.8.2 h1:52wnefTJnPI5FoHif1DQh2soKRw0yYs+4AVyvtcZCH0=
-github.com/montanaflynn/stats v0.8.2/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
+github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
+github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -353,8 +348,8 @@ github.com/opencontainers/runc v1.3.3 h1:qlmBbbhu+yY0QM7jqfuat7M1H3/iXjju3VkP9lk
github.com/opencontainers/runc v1.3.3/go.mod h1:D7rL72gfWxVs9cJ2/AayxB0Hlvn9g0gaF1R7uunumSI=
github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw=
github.com/ory/dockertest/v3 v3.12.0/go.mod h1:aKNDTva3cp8dwOWwb9cWuX84aH5akkxXRvO7KCwWVjE=
-github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
-github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
+github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
@@ -384,8 +379,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
-github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
-github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
+github.com/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
+github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
@@ -464,42 +459,42 @@ github.com/zinclabs/bluge_segment_api v1.0.0 h1:GJvPxdzR7KjwdxmcKleQLvtIYi/J7Q7e
github.com/zinclabs/bluge_segment_api v1.0.0/go.mod h1:mYfPVUdXLZ4iXsicXMER+RcI/avwphjMOi8nhN9HDLA=
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
-go.etcd.io/etcd/api/v3 v3.6.9 h1:UA7iKfEW1AzgihcBSGXci2kDGQiokSq41F9HMCI/RTI=
-go.etcd.io/etcd/api/v3 v3.6.9/go.mod h1:csEk/qTfxKL36NqJdU15Tgtl65A8dyEY2BYo7PRsIwk=
-go.etcd.io/etcd/client/pkg/v3 v3.6.9 h1:T8nuk8Lz64C+Hzb0coBFLMSlVSQZBpAtFk46swdM1DA=
-go.etcd.io/etcd/client/pkg/v3 v3.6.9/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y=
-go.etcd.io/etcd/client/v3 v3.6.9 h1:3X555hQXmhRr27O37wls53g68CpUiPOiHXrZfz2Al+o=
-go.etcd.io/etcd/client/v3 v3.6.9/go.mod h1:KO7H1HLYh1qaljuVZJQwBFk1lRce6pJzt+C81GEnrlM=
-go.etcd.io/etcd/pkg/v3 v3.6.9 h1:k3LEWBVQHTtYYd/F1wG1vC/A5NxGYw8aoVgZIbn1sJ4=
-go.etcd.io/etcd/pkg/v3 v3.6.9/go.mod h1:gUV6SFKRWMJSGCAJ8EYGOlbQYTlP2RZtqxQYWKpgsKw=
-go.etcd.io/etcd/server/v3 v3.6.9 h1:4vDo2aJTm+wa9DZp+2IyvA1BqXVcLXv5FBe7sNeogxw=
-go.etcd.io/etcd/server/v3 v3.6.9/go.mod h1:jMDuXmvgku6FnrGykmYhYkW+K1Hd3IwFKGgBM5o3o2c=
+go.etcd.io/etcd/api/v3 v3.6.10 h1:jlwjtELjA8yi2VWpOFH+0w0lGr3K6mVDyn0RDB9aaAY=
+go.etcd.io/etcd/api/v3 v3.6.10/go.mod h1:pdV4VeFmvhdNjB4LWRkC8ReLyRBAxUOze3GarMhE2sk=
+go.etcd.io/etcd/client/pkg/v3 v3.6.10 h1:tBT7podcPhuVbCVkAEzx8bC5I+aqxfLwBN8/As1arrA=
+go.etcd.io/etcd/client/pkg/v3 v3.6.10/go.mod h1:WEy3PpwbbEBVRdh1NVJYsuUe/8eyI21PNJRazeD8z/Y=
+go.etcd.io/etcd/client/v3 v3.6.10 h1:J598zJ+C/ZPvImypmq5waj84+bovePrlZERHklf34y0=
+go.etcd.io/etcd/client/v3 v3.6.10/go.mod h1:iHhUDUcEwaKs1YFq3MgmI9U4zhTVasp/vgdVbFf1RS8=
+go.etcd.io/etcd/pkg/v3 v3.6.10 h1:17TwO17SkbgIBCloGRikqVy/HdPbeXX4zPAlzRZbzDw=
+go.etcd.io/etcd/pkg/v3 v3.6.10/go.mod h1:UfjvwrKPZVnmpKD3UZKQpC7kMLsSIGlQjIVFGRXI6eo=
+go.etcd.io/etcd/server/v3 v3.6.10 h1:MfnewdUHfYUr7ytYKuYQb2WWxTstGj70Vx7nM8e2rVs=
+go.etcd.io/etcd/server/v3 v3.6.10/go.mod h1:dWedTPxlXqd9P4pxgqjSb2NwcqobmumsMWLOf4Z+OTY=
go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ=
go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
-go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
-go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
-go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
-go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
-go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU=
-go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0 h1:ZrPRak/kS4xI3AVXy8F7pipuDXmDsrO8Lg+yQjBLjw0=
-go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.40.0/go.mod h1:3y6kQCWztq6hyW8Z9YxQDDm0Je9AJoFar2G0yDcmhRk=
-go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
-go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
-go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
-go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
-go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
-go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
-go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
-go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
+go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU=
+go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
+go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
+go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 h1:RAE+JPfvEmvy+0LzyUA25/SGawPwIUbZ6u0Wug54sLc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0/go.mod h1:AGmbycVGEsRx9mXMZ75CsOyhSP6MFIcj/6dnG+vhVjk=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0 h1:lSZHgNHfbmQTPfuTmWVkEu8J8qXaQwuV30pjCcAUvP8=
+go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.42.0/go.mod h1:so9ounLcuoRDu033MW/E0AD4hhUjVqswrMF5FoZlBcw=
+go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
+go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
+go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
+go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
+go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
+go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
+go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
+go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
@@ -552,12 +547,9 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
-golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
+golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -576,18 +568,18 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
-gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
-google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA=
-google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA=
-google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
-google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
-google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7 h1:41r6JMbpzBMen0R/4TZeeAmGXSJC7DftGINUodzTkPI=
-google.golang.org/genproto/googleapis/api v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7 h1:ndE4FoJqsIceKP2oYSnUZqhTdYufCYYkqwtFzfrhI7w=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
-google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
-google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
+gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
+gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
+google.golang.org/api v0.275.0 h1:vfY5d9vFVJeWEZT65QDd9hbndr7FyZ2+6mIzGAh71NI=
+google.golang.org/api v0.275.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
+google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d h1:N1Ec54vZnIPd7MnxRiYLW+oY4fDR4BOS/LrssdD9+ek=
+google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:c2hJ1grtnH0xUiEKGDGkjGNTJ1Hy2LrblyKOHF0sqRM=
+google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d h1:/aDRtSZJjyLQzm75d+a1wOJaqyKBMvIAfeQmoa3ORiI=
+google.golang.org/genproto/googleapis/api v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:etfGUgejTiadZAUaEP14NP97xi1RGeawqkjDARA/UOs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d h1:wT2n40TBqFY6wiwazVK9/iTWbsQrgk5ZfCSVFLO9LQA=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
+google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/mcp/LICENSE b/mcp/LICENSE
index e98f9981b..46b91819a 100644
--- a/mcp/LICENSE
+++ b/mcp/LICENSE
@@ -36,7 +36,7 @@ BSD-3-Clause licenses
========================================================================
fast-uri 3.1.0 BSD-3-Clause
- qs 6.15.0 BSD-3-Clause
+ qs 6.15.1 BSD-3-Clause
========================================================================
ISC licenses
@@ -48,14 +48,14 @@ ISC licenses
setprototypeof 1.2.0 ISC
which 2.0.2 ISC
wrappy 1.0.2 ISC
- zod-to-json-schema 3.25.1 ISC
+ zod-to-json-schema 3.25.2 ISC
========================================================================
MIT licenses
========================================================================
- @hono/node-server 1.19.11 MIT
- @modelcontextprotocol/sdk 1.28.0 MIT
+ @hono/node-server 1.19.13 MIT
+ @modelcontextprotocol/sdk 1.29.0 MIT
accepts 2.0.0 MIT
ajv 8.18.0 MIT
ajv-formats 3.0.1 MIT
@@ -63,7 +63,7 @@ MIT licenses
bytes 3.1.2 MIT
call-bind-apply-helpers 1.0.2 MIT
call-bound 1.0.4 MIT
- content-disposition 1.0.1 MIT
+ content-disposition 1.1.0 MIT
content-type 1.0.5 MIT
cookie 0.7.2 MIT
cookie-signature 1.2.2 MIT
@@ -82,7 +82,7 @@ MIT licenses
eventsource 3.0.7 MIT
eventsource-parser 3.0.6 MIT
express 5.2.1 MIT
- express-rate-limit 8.3.1 MIT
+ express-rate-limit 8.3.2 MIT
fast-deep-equal 3.1.3 MIT
finalhandler 2.1.1 MIT
forwarded 0.2.0 MIT
@@ -93,7 +93,7 @@ MIT licenses
gopd 1.2.0 MIT
has-symbols 1.1.0 MIT
hasown 2.0.2 MIT
- hono 4.12.9 MIT
+ hono 4.12.12 MIT
http-errors 2.0.1 MIT
iconv-lite 0.7.2 MIT
ip-address 10.1.0 MIT
@@ -113,7 +113,7 @@ MIT licenses
on-finished 2.4.1 MIT
parseurl 1.3.3 MIT
path-key 3.1.1 MIT
- path-to-regexp 8.4.1 MIT
+ path-to-regexp 8.4.2 MIT
pkce-challenge 5.0.1 MIT
proxy-addr 2.0.7 MIT
range-parser 1.2.1 MIT
@@ -126,7 +126,7 @@ MIT licenses
shebang-command 2.0.0 MIT
shebang-regex 3.0.0 MIT
side-channel 1.1.0 MIT
- side-channel-list 1.0.0 MIT
+ side-channel-list 1.0.1 MIT
side-channel-map 1.0.1 MIT
side-channel-weakmap 1.0.2 MIT
statuses 2.0.2 MIT
diff --git a/mcp/package-lock.json b/mcp/package-lock.json
index 348ffbc28..7ea6249cc 100644
--- a/mcp/package-lock.json
+++ b/mcp/package-lock.json
@@ -30,9 +30,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
- "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
+ "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
@@ -47,9 +47,9 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
- "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
+ "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
@@ -64,9 +64,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
- "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
+ "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
@@ -81,9 +81,9 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
- "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
+ "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
@@ -98,9 +98,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
- "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
+ "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
@@ -115,9 +115,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
- "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
+ "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
@@ -132,9 +132,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
- "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
@@ -149,9 +149,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
- "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
+ "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
@@ -166,9 +166,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
- "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
+ "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
@@ -183,9 +183,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
- "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
+ "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
@@ -200,9 +200,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
- "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
+ "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
@@ -217,9 +217,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
- "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
+ "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
@@ -234,9 +234,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
- "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
+ "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
@@ -251,9 +251,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
- "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
+ "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
@@ -268,9 +268,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
- "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
+ "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
@@ -285,9 +285,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
- "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
+ "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
@@ -302,9 +302,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
- "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
+ "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
@@ -319,9 +319,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
- "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
@@ -336,9 +336,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
- "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
@@ -353,9 +353,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
- "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
+ "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
@@ -370,9 +370,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
- "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
+ "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
@@ -387,9 +387,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
- "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
+ "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
@@ -404,9 +404,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
- "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
+ "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
@@ -421,9 +421,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
- "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
+ "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
@@ -438,9 +438,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
- "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
+ "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
@@ -455,9 +455,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
- "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
+ "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
@@ -699,9 +699,9 @@
}
},
"node_modules/@hono/node-server": {
- "version": "1.19.11",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
- "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
+ "version": "1.19.13",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.13.tgz",
+ "integrity": "sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -763,9 +763,9 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.28.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.28.0.tgz",
- "integrity": "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==",
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
+ "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
"license": "MIT",
"dependencies": {
"@hono/node-server": "^1.19.9",
@@ -817,9 +817,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.12.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz",
- "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+ "version": "24.12.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
+ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -827,20 +827,20 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
- "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz",
+ "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.57.2",
- "@typescript-eslint/type-utils": "8.57.2",
- "@typescript-eslint/utils": "8.57.2",
- "@typescript-eslint/visitor-keys": "8.57.2",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/type-utils": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
- "ts-api-utils": "^2.4.0"
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -850,22 +850,22 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.57.2",
+ "@typescript-eslint/parser": "^8.58.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz",
- "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz",
+ "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.57.2",
- "@typescript-eslint/types": "8.57.2",
- "@typescript-eslint/typescript-estree": "8.57.2",
- "@typescript-eslint/visitor-keys": "8.57.2",
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
"debug": "^4.4.3"
},
"engines": {
@@ -877,18 +877,18 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz",
- "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz",
+ "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.57.2",
- "@typescript-eslint/types": "^8.57.2",
+ "@typescript-eslint/tsconfig-utils": "^8.58.1",
+ "@typescript-eslint/types": "^8.58.1",
"debug": "^4.4.3"
},
"engines": {
@@ -899,18 +899,18 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz",
- "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz",
+ "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.57.2",
- "@typescript-eslint/visitor-keys": "8.57.2"
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -921,9 +921,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz",
- "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz",
+ "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -934,21 +934,21 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz",
- "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz",
+ "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.57.2",
- "@typescript-eslint/typescript-estree": "8.57.2",
- "@typescript-eslint/utils": "8.57.2",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1",
"debug": "^4.4.3",
- "ts-api-utils": "^2.4.0"
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -959,13 +959,13 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz",
- "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz",
+ "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -977,21 +977,21 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz",
- "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz",
+ "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.57.2",
- "@typescript-eslint/tsconfig-utils": "8.57.2",
- "@typescript-eslint/types": "8.57.2",
- "@typescript-eslint/visitor-keys": "8.57.2",
+ "@typescript-eslint/project-service": "8.58.1",
+ "@typescript-eslint/tsconfig-utils": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/visitor-keys": "8.58.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
"tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.4.0"
+ "ts-api-utils": "^2.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1001,20 +1001,20 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz",
- "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz",
+ "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.57.2",
- "@typescript-eslint/types": "8.57.2",
- "@typescript-eslint/typescript-estree": "8.57.2"
+ "@typescript-eslint/scope-manager": "8.58.1",
+ "@typescript-eslint/types": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1025,17 +1025,17 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz",
- "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz",
+ "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/types": "8.58.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1291,9 +1291,9 @@
"license": "MIT"
},
"node_modules/content-disposition": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
- "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
+ "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
"license": "MIT",
"engines": {
"node": ">=18"
@@ -1466,9 +1466,9 @@
}
},
"node_modules/esbuild": {
- "version": "0.27.4",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
- "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
+ "version": "0.27.7",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
+ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1479,32 +1479,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.4",
- "@esbuild/android-arm": "0.27.4",
- "@esbuild/android-arm64": "0.27.4",
- "@esbuild/android-x64": "0.27.4",
- "@esbuild/darwin-arm64": "0.27.4",
- "@esbuild/darwin-x64": "0.27.4",
- "@esbuild/freebsd-arm64": "0.27.4",
- "@esbuild/freebsd-x64": "0.27.4",
- "@esbuild/linux-arm": "0.27.4",
- "@esbuild/linux-arm64": "0.27.4",
- "@esbuild/linux-ia32": "0.27.4",
- "@esbuild/linux-loong64": "0.27.4",
- "@esbuild/linux-mips64el": "0.27.4",
- "@esbuild/linux-ppc64": "0.27.4",
- "@esbuild/linux-riscv64": "0.27.4",
- "@esbuild/linux-s390x": "0.27.4",
- "@esbuild/linux-x64": "0.27.4",
- "@esbuild/netbsd-arm64": "0.27.4",
- "@esbuild/netbsd-x64": "0.27.4",
- "@esbuild/openbsd-arm64": "0.27.4",
- "@esbuild/openbsd-x64": "0.27.4",
- "@esbuild/openharmony-arm64": "0.27.4",
- "@esbuild/sunos-x64": "0.27.4",
- "@esbuild/win32-arm64": "0.27.4",
- "@esbuild/win32-ia32": "0.27.4",
- "@esbuild/win32-x64": "0.27.4"
+ "@esbuild/aix-ppc64": "0.27.7",
+ "@esbuild/android-arm": "0.27.7",
+ "@esbuild/android-arm64": "0.27.7",
+ "@esbuild/android-x64": "0.27.7",
+ "@esbuild/darwin-arm64": "0.27.7",
+ "@esbuild/darwin-x64": "0.27.7",
+ "@esbuild/freebsd-arm64": "0.27.7",
+ "@esbuild/freebsd-x64": "0.27.7",
+ "@esbuild/linux-arm": "0.27.7",
+ "@esbuild/linux-arm64": "0.27.7",
+ "@esbuild/linux-ia32": "0.27.7",
+ "@esbuild/linux-loong64": "0.27.7",
+ "@esbuild/linux-mips64el": "0.27.7",
+ "@esbuild/linux-ppc64": "0.27.7",
+ "@esbuild/linux-riscv64": "0.27.7",
+ "@esbuild/linux-s390x": "0.27.7",
+ "@esbuild/linux-x64": "0.27.7",
+ "@esbuild/netbsd-arm64": "0.27.7",
+ "@esbuild/netbsd-x64": "0.27.7",
+ "@esbuild/openbsd-arm64": "0.27.7",
+ "@esbuild/openbsd-x64": "0.27.7",
+ "@esbuild/openharmony-arm64": "0.27.7",
+ "@esbuild/sunos-x64": "0.27.7",
+ "@esbuild/win32-arm64": "0.27.7",
+ "@esbuild/win32-ia32": "0.27.7",
+ "@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/escape-html": {
@@ -1845,9 +1845,9 @@
}
},
"node_modules/express-rate-limit": {
- "version": "8.3.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.1.tgz",
- "integrity": "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==",
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
+ "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"license": "MIT",
"dependencies": {
"ip-address": "10.1.0"
@@ -2161,9 +2161,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.9",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
- "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
+ "version": "4.12.12",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
+ "integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -2465,13 +2465,13 @@
}
},
"node_modules/minimatch": {
- "version": "10.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
- "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^5.0.2"
+ "brace-expansion": "^5.0.5"
},
"engines": {
"node": "18 || 20 || >=22"
@@ -2636,9 +2636,9 @@
}
},
"node_modules/path-to-regexp": {
- "version": "8.4.1",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.1.tgz",
- "integrity": "sha512-fvU78fIjZ+SBM9YwCknCvKOUKkLVqtWDVctl0s7xIqfmfb38t2TT4ZU2gHm+Z8xGwgW+QWEU3oQSAzIbo89Ggw==",
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
+ "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -2717,9 +2717,9 @@
}
},
"node_modules/qs": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
- "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -2911,13 +2911,13 @@
}
},
"node_modules/side-channel-list": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
- "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
- "object-inspect": "^1.13.3"
+ "object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -2999,14 +2999,14 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -3099,16 +3099,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.57.2",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz",
- "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==",
+ "version": "8.58.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.1.tgz",
+ "integrity": "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.57.2",
- "@typescript-eslint/parser": "8.57.2",
- "@typescript-eslint/typescript-estree": "8.57.2",
- "@typescript-eslint/utils": "8.57.2"
+ "@typescript-eslint/eslint-plugin": "8.58.1",
+ "@typescript-eslint/parser": "8.58.1",
+ "@typescript-eslint/typescript-estree": "8.58.1",
+ "@typescript-eslint/utils": "8.58.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -3119,7 +3119,7 @@
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.0.0"
+ "typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/undici-types": {
@@ -3221,12 +3221,12 @@
}
},
"node_modules/zod-to-json-schema": {
- "version": "3.25.1",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
- "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
"license": "ISC",
"peerDependencies": {
- "zod": "^3.25 || ^4"
+ "zod": "^3.25.28 || ^4"
}
}
}
diff --git a/ui/LICENSE b/ui/LICENSE
index 9236572a1..1e0722d87 100644
--- a/ui/LICENSE
+++ b/ui/LICENSE
@@ -74,21 +74,21 @@ MIT licenses
@parcel/watcher-linux-x64-musl 2.5.6 MIT
@popperjs/core 2.11.8 MIT
@tanstack/match-sorter-utils 8.19.4 MIT
- @tanstack/query-core 5.95.2 MIT
- @tanstack/vue-query 5.95.2 MIT
+ @tanstack/query-core 5.97.0 MIT
+ @tanstack/vue-query 5.97.0 MIT
@types/lodash 4.17.24 MIT
@types/lodash-es 4.17.12 MIT
@types/web-bluetooth 0.0.20 MIT
- @vue/compiler-core 3.5.31 MIT
- @vue/compiler-dom 3.5.31 MIT
- @vue/compiler-sfc 3.5.31 MIT
- @vue/compiler-ssr 3.5.31 MIT
+ @vue/compiler-core 3.5.32 MIT
+ @vue/compiler-dom 3.5.32 MIT
+ @vue/compiler-sfc 3.5.32 MIT
+ @vue/compiler-ssr 3.5.32 MIT
@vue/devtools-api 6.6.4 MIT
- @vue/reactivity 3.5.31 MIT
- @vue/runtime-core 3.5.31 MIT
- @vue/runtime-dom 3.5.31 MIT
- @vue/server-renderer 3.5.31 MIT
- @vue/shared 3.5.31 MIT
+ @vue/reactivity 3.5.32 MIT
+ @vue/runtime-core 3.5.32 MIT
+ @vue/runtime-dom 3.5.32 MIT
+ @vue/server-renderer 3.5.32 MIT
+ @vue/shared 3.5.32 MIT
@vueuse/core 12.0.0 MIT
@vueuse/metadata 12.0.0 MIT
@vueuse/shared 12.0.0 MIT
@@ -118,18 +118,18 @@ MIT licenses
node-addon-api 7.1.1 MIT
picomatch 4.0.4 MIT
pinia 2.3.1 MIT
- postcss 8.5.8 MIT
+ postcss 8.5.9 MIT
process-nextick-args 2.0.1 MIT
readable-stream 2.3.8 MIT
readdirp 4.1.2 MIT
remove-accents 0.5.0 MIT
safe-buffer 5.1.2 MIT
- sass 1.98.0 MIT
+ sass 1.99.0 MIT
setimmediate 1.0.5 MIT
string_decoder 1.1.1 MIT
universalify 2.0.1 MIT
util-deprecate 1.0.2 MIT
- vue 3.5.31 MIT
+ vue 3.5.32 MIT
vue-component-type-helpers 3.2.6 MIT
vue-demi 0.14.10 MIT
vue-router 4.6.4 MIT
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 6a778b7b0..64e6259ff 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -951,9 +951,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz",
- "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz",
+ "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==",
"cpu": [
"arm"
],
@@ -965,9 +965,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz",
- "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz",
+ "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==",
"cpu": [
"arm64"
],
@@ -979,9 +979,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz",
- "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz",
+ "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==",
"cpu": [
"arm64"
],
@@ -993,9 +993,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz",
- "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz",
+ "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==",
"cpu": [
"x64"
],
@@ -1007,9 +1007,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz",
- "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz",
+ "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==",
"cpu": [
"arm64"
],
@@ -1021,9 +1021,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz",
- "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz",
+ "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==",
"cpu": [
"x64"
],
@@ -1035,9 +1035,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz",
- "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz",
+ "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==",
"cpu": [
"arm"
],
@@ -1049,9 +1049,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz",
- "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz",
+ "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==",
"cpu": [
"arm"
],
@@ -1063,9 +1063,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz",
- "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz",
+ "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==",
"cpu": [
"arm64"
],
@@ -1077,9 +1077,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz",
- "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz",
+ "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==",
"cpu": [
"arm64"
],
@@ -1091,9 +1091,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz",
- "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz",
+ "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==",
"cpu": [
"loong64"
],
@@ -1105,9 +1105,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz",
- "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz",
+ "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==",
"cpu": [
"loong64"
],
@@ -1119,9 +1119,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz",
- "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz",
+ "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==",
"cpu": [
"ppc64"
],
@@ -1133,9 +1133,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz",
- "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz",
+ "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==",
"cpu": [
"ppc64"
],
@@ -1147,9 +1147,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz",
- "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz",
+ "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==",
"cpu": [
"riscv64"
],
@@ -1161,9 +1161,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz",
- "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz",
+ "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==",
"cpu": [
"riscv64"
],
@@ -1175,9 +1175,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz",
- "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz",
+ "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==",
"cpu": [
"s390x"
],
@@ -1189,9 +1189,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz",
- "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==",
"cpu": [
"x64"
],
@@ -1203,9 +1203,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz",
- "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz",
+ "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==",
"cpu": [
"x64"
],
@@ -1217,9 +1217,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz",
- "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz",
+ "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==",
"cpu": [
"x64"
],
@@ -1231,9 +1231,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz",
- "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz",
+ "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==",
"cpu": [
"arm64"
],
@@ -1245,9 +1245,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz",
- "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz",
+ "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==",
"cpu": [
"arm64"
],
@@ -1259,9 +1259,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz",
- "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz",
+ "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==",
"cpu": [
"ia32"
],
@@ -1273,9 +1273,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz",
- "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz",
+ "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==",
"cpu": [
"x64"
],
@@ -1287,9 +1287,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz",
- "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz",
+ "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==",
"cpu": [
"x64"
],
@@ -1317,9 +1317,9 @@
}
},
"node_modules/@tanstack/query-core": {
- "version": "5.95.2",
- "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz",
- "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==",
+ "version": "5.97.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.97.0.tgz",
+ "integrity": "sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -1327,13 +1327,13 @@
}
},
"node_modules/@tanstack/vue-query": {
- "version": "5.95.2",
- "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.95.2.tgz",
- "integrity": "sha512-GleO0GrUPdvObtff/D3iQ5kUERQM3dM6vT5pWl4zC3ap2JO84x4SQbUa1G7czKx96lETRiHnw7ZuatSRaaZqQQ==",
+ "version": "5.97.0",
+ "resolved": "https://registry.npmjs.org/@tanstack/vue-query/-/vue-query-5.97.0.tgz",
+ "integrity": "sha512-lziGgJTrEVXX98nQ9D+MfvjHgD92uHPrVXjB1GSfK+mq0rTr/I0ovwraGqaNTfwORmzHtQOObksPVf2NH17djw==",
"license": "MIT",
"dependencies": {
"@tanstack/match-sorter-utils": "^8.19.4",
- "@tanstack/query-core": "5.95.2",
+ "@tanstack/query-core": "5.97.0",
"@vue/devtools-api": "^6.6.3",
"vue-demi": "^0.14.10"
},
@@ -1394,39 +1394,39 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.31.tgz",
- "integrity": "sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.32.tgz",
+ "integrity": "sha512-4x74Tbtqnda8s/NSD6e1Dr5p1c8HdMU5RWSjMSUzb8RTcUQqevDCxVAitcLBKT+ie3o0Dl9crc/S/opJM7qBGQ==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.2",
- "@vue/shared": "3.5.31",
+ "@vue/shared": "3.5.32",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz",
- "integrity": "sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.32.tgz",
+ "integrity": "sha512-ybHAu70NtiEI1fvAUz3oXZqkUYEe5J98GjMDpTGl5iHb0T15wQYLR4wE3h9xfuTNA+Cm2f4czfe8B4s+CCH57Q==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.31",
- "@vue/shared": "3.5.31"
+ "@vue/compiler-core": "3.5.32",
+ "@vue/shared": "3.5.32"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz",
- "integrity": "sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.32.tgz",
+ "integrity": "sha512-8UYUYo71cP/0YHMO814TRZlPuUUw3oifHuMR7Wp9SNoRSrxRQnhMLNlCeaODNn6kNTJsjFoQ/kqIj4qGvya4Xg==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.2",
- "@vue/compiler-core": "3.5.31",
- "@vue/compiler-dom": "3.5.31",
- "@vue/compiler-ssr": "3.5.31",
- "@vue/shared": "3.5.31",
+ "@vue/compiler-core": "3.5.32",
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/compiler-ssr": "3.5.32",
+ "@vue/shared": "3.5.32",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.8",
@@ -1434,13 +1434,13 @@
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz",
- "integrity": "sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.32.tgz",
+ "integrity": "sha512-Gp4gTs22T3DgRotZ8aA/6m2jMR+GMztvBXUBEUOYOcST+giyGWJ4WvFd7QLHBkzTxkfOt8IELKNdpzITLbA2rw==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.31",
- "@vue/shared": "3.5.31"
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/shared": "3.5.32"
}
},
"node_modules/@vue/devtools-api": {
@@ -1450,53 +1450,53 @@
"license": "MIT"
},
"node_modules/@vue/reactivity": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.31.tgz",
- "integrity": "sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.32.tgz",
+ "integrity": "sha512-/ORasxSGvZ6MN5gc+uE364SxFdJ0+WqVG0CENXaGW58TOCdrAW76WWaplDtECeS1qphvtBZtR+3/o1g1zL4xPQ==",
"license": "MIT",
"dependencies": {
- "@vue/shared": "3.5.31"
+ "@vue/shared": "3.5.32"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.31.tgz",
- "integrity": "sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.32.tgz",
+ "integrity": "sha512-pDrXCejn4UpFDFmMd27AcJEbHaLemaE5o4pbb7sLk79SRIhc6/t34BQA7SGNgYtbMnvbF/HHOftYBgFJtUoJUQ==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.31",
- "@vue/shared": "3.5.31"
+ "@vue/reactivity": "3.5.32",
+ "@vue/shared": "3.5.32"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz",
- "integrity": "sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.32.tgz",
+ "integrity": "sha512-1CDVv7tv/IV13V8Nip1k/aaObVbWqRlVCVezTwx3K07p7Vxossp5JU1dcPNhJk3w347gonIUT9jQOGutyJrSVQ==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.31",
- "@vue/runtime-core": "3.5.31",
- "@vue/shared": "3.5.31",
+ "@vue/reactivity": "3.5.32",
+ "@vue/runtime-core": "3.5.32",
+ "@vue/shared": "3.5.32",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.31.tgz",
- "integrity": "sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.32.tgz",
+ "integrity": "sha512-IOjm2+JQwRFS7W28HNuJeXQle9KdZbODFY7hFGVtnnghF51ta20EWAZJHX+zLGtsHhaU6uC9BGPV52KVpYryMQ==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.5.31",
- "@vue/shared": "3.5.31"
+ "@vue/compiler-ssr": "3.5.32",
+ "@vue/shared": "3.5.32"
},
"peerDependencies": {
- "vue": "3.5.31"
+ "vue": "3.5.32"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.31.tgz",
- "integrity": "sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.32.tgz",
+ "integrity": "sha512-ksNyrmRQzWJJ8n3cRDuSF7zNNontuJg1YHnmWRJd2AMu8Ij2bqwiiri2lH5rHtYPZjj4STkNcgcmiQqlOjiYGg==",
"license": "MIT"
},
"node_modules/@vueuse/core": {
@@ -2323,9 +2323,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.8",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
- "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "version": "8.5.9",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz",
+ "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==",
"funding": [
{
"type": "opencollective",
@@ -2477,9 +2477,9 @@
}
},
"node_modules/rollup": {
- "version": "4.60.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz",
- "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==",
+ "version": "4.60.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
+ "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2493,31 +2493,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.0",
- "@rollup/rollup-android-arm64": "4.60.0",
- "@rollup/rollup-darwin-arm64": "4.60.0",
- "@rollup/rollup-darwin-x64": "4.60.0",
- "@rollup/rollup-freebsd-arm64": "4.60.0",
- "@rollup/rollup-freebsd-x64": "4.60.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.0",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.0",
- "@rollup/rollup-linux-arm64-gnu": "4.60.0",
- "@rollup/rollup-linux-arm64-musl": "4.60.0",
- "@rollup/rollup-linux-loong64-gnu": "4.60.0",
- "@rollup/rollup-linux-loong64-musl": "4.60.0",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.0",
- "@rollup/rollup-linux-ppc64-musl": "4.60.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.0",
- "@rollup/rollup-linux-riscv64-musl": "4.60.0",
- "@rollup/rollup-linux-s390x-gnu": "4.60.0",
- "@rollup/rollup-linux-x64-gnu": "4.60.0",
- "@rollup/rollup-linux-x64-musl": "4.60.0",
- "@rollup/rollup-openbsd-x64": "4.60.0",
- "@rollup/rollup-openharmony-arm64": "4.60.0",
- "@rollup/rollup-win32-arm64-msvc": "4.60.0",
- "@rollup/rollup-win32-ia32-msvc": "4.60.0",
- "@rollup/rollup-win32-x64-gnu": "4.60.0",
- "@rollup/rollup-win32-x64-msvc": "4.60.0",
+ "@rollup/rollup-android-arm-eabi": "4.60.1",
+ "@rollup/rollup-android-arm64": "4.60.1",
+ "@rollup/rollup-darwin-arm64": "4.60.1",
+ "@rollup/rollup-darwin-x64": "4.60.1",
+ "@rollup/rollup-freebsd-arm64": "4.60.1",
+ "@rollup/rollup-freebsd-x64": "4.60.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.60.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.60.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.60.1",
+ "@rollup/rollup-linux-arm64-musl": "4.60.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.60.1",
+ "@rollup/rollup-linux-loong64-musl": "4.60.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.60.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.60.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.60.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.60.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-gnu": "4.60.1",
+ "@rollup/rollup-linux-x64-musl": "4.60.1",
+ "@rollup/rollup-openbsd-x64": "4.60.1",
+ "@rollup/rollup-openharmony-arm64": "4.60.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.60.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.60.1",
+ "@rollup/rollup-win32-x64-gnu": "4.60.1",
+ "@rollup/rollup-win32-x64-msvc": "4.60.1",
"fsevents": "~2.3.2"
}
},
@@ -2552,9 +2552,9 @@
"license": "MIT"
},
"node_modules/sass": {
- "version": "1.98.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz",
- "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
+ "version": "1.99.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
+ "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
@@ -2641,14 +2641,14 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.15",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
- "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
@@ -2920,9 +2920,9 @@
"license": "MIT"
},
"node_modules/vite": {
- "version": "6.4.1",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
- "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "version": "6.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz",
+ "integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2995,16 +2995,16 @@
}
},
"node_modules/vue": {
- "version": "3.5.31",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.31.tgz",
- "integrity": "sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==",
+ "version": "3.5.32",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.32.tgz",
+ "integrity": "sha512-vM4z4Q9tTafVfMAK7IVzmxg34rSzTFMyIe0UUEijUCkn9+23lj0WRfA83dg7eQZIUlgOSGrkViIaCfqSAUXsMw==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.31",
- "@vue/compiler-sfc": "3.5.31",
- "@vue/runtime-dom": "3.5.31",
- "@vue/server-renderer": "3.5.31",
- "@vue/shared": "3.5.31"
+ "@vue/compiler-dom": "3.5.32",
+ "@vue/compiler-sfc": "3.5.32",
+ "@vue/runtime-dom": "3.5.32",
+ "@vue/server-renderer": "3.5.32",
+ "@vue/shared": "3.5.32"
},
"peerDependencies": {
"typescript": "*"
(skywalking-banyandb) 21/24: fix(fodc): heal reconnect deadlock and add error message when no agents for lifecycle request (#1101)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 4178a498375b77d0725f0e8fc1651b6b62c8f0d4
Author: mrproliu <74...@qq.com>
AuthorDate: Wed Apr 29 10:38:22 2026 +0800
fix(fodc): heal reconnect deadlock and add error message when no agents for lifecycle request (#1101)
---
fodc/agent/internal/proxy/client.go | 65 ++++++---
.../internal/proxy/client_blocking_send_test.go | 151 +++++++++++++++++++++
fodc/agent/internal/proxy/client_test.go | 13 ++
fodc/agent/internal/proxy/wait.go | 69 ++++++++++
fodc/agent/internal/proxy/wait_test.go | 61 +++++++++
fodc/proxy/internal/api/server.go | 30 +++-
fodc/proxy/internal/api/server_test.go | 52 +++++++
fodc/proxy/internal/lifecycle/manager.go | 112 ++++++++++-----
fodc/proxy/internal/lifecycle/manager_test.go | 31 ++++-
9 files changed, 521 insertions(+), 63 deletions(-)
diff --git a/fodc/agent/internal/proxy/client.go b/fodc/agent/internal/proxy/client.go
index cf7b44203..26752365d 100644
--- a/fodc/agent/internal/proxy/client.go
+++ b/fodc/agent/internal/proxy/client.go
@@ -130,7 +130,9 @@ func (c *Client) Connect(ctx context.Context) error {
c.streamsMu.Unlock()
if wasDisconnected {
- c.heartbeatWg.Wait()
+ if !waitWaitGroupWithTimeout(&c.heartbeatWg, heartbeatWaitTimeout) {
+ c.logger.Warn().Msg("Heartbeat goroutine did not exit in time during Connect, proceeding anyway")
+ }
}
c.streamsMu.Lock()
@@ -693,8 +695,11 @@ func (c *Client) sendFilteredMetrics(
return nil
}
-// SendHeartbeat sends heartbeat to Proxy.
-func (c *Client) SendHeartbeat(_ context.Context) error {
+// SendHeartbeat sends heartbeat to Proxy. The provided context bounds the wait so that a
+// stream whose Send is wedged (for example, after the proxy entered graceful_stop and the
+// underlying TCP write queue is no longer being drained) cannot pin the heartbeat goroutine
+// indefinitely.
+func (c *Client) SendHeartbeat(ctx context.Context) error {
c.streamsMu.RLock()
if c.disconnected || c.registrationStream == nil {
c.streamsMu.RUnlock()
@@ -710,7 +715,23 @@ func (c *Client) SendHeartbeat(_ context.Context) error {
ContainerNames: c.containerNames,
}
- if sendErr := registrationStream.Send(req); sendErr != nil {
+ // gRPC client-stream Send does not accept a context, so wrap it in a goroutine and
+ // race the result against ctx.Done(). When ctx fires first the in-flight Send is
+ // orphaned. The leak is bounded: every reconnect/cleanupStreams/Disconnect path
+ // closes the registration stream after waiting on heartbeatWg with bounded timeout,
+ // which forces Send to return so the orphan goroutine can write to the (buffered)
+ // channel and exit. Without an upstream tear-down the orphan would persist, so the
+ // reliability of the heartbeatWaitTimeout-bounded cleanup paths is load-bearing here.
+ sendErrCh := make(chan error, 1)
+ go func() { sendErrCh <- registrationStream.Send(req) }()
+
+ var sendErr error
+ select {
+ case sendErr = <-sendErrCh:
+ case <-ctx.Done():
+ return fmt.Errorf("heartbeat send aborted: %w", ctx.Err())
+ }
+ if sendErr != nil {
// Check if error is due to stream being closed/disconnected
if errors.Is(sendErr, io.EOF) || errors.Is(sendErr, context.Canceled) {
return fmt.Errorf("registration stream closed")
@@ -738,15 +759,11 @@ func (c *Client) cleanupStreams() {
c.stopCh = make(chan struct{})
c.streamsMu.Unlock()
- if oldStopCh != nil {
- select {
- case <-oldStopCh:
- default:
- close(oldStopCh)
- }
- }
+ safeClose(oldStopCh)
- c.heartbeatWg.Wait()
+ if !waitWaitGroupWithTimeout(&c.heartbeatWg, heartbeatWaitTimeout) {
+ c.logger.Warn().Msg("Heartbeat goroutine did not exit in time during cleanupStreams, proceeding anyway")
+ }
c.streamsMu.Lock()
if c.registrationStream != nil {
@@ -798,8 +815,11 @@ func (c *Client) Disconnect() error {
c.streamsMu.Unlock()
- // Wait for heartbeat goroutine to exit before closing streams
- c.heartbeatWg.Wait()
+ // Wait for heartbeat goroutine to exit before closing streams. Bounded so a wedged
+ // in-flight Send cannot keep Disconnect from progressing.
+ if !waitWaitGroupWithTimeout(&c.heartbeatWg, heartbeatWaitTimeout) {
+ c.logger.Warn().Msg("Heartbeat goroutine did not exit in time during Disconnect, proceeding anyway")
+ }
c.streamsMu.Lock()
if c.registrationStream != nil {
@@ -1017,15 +1037,22 @@ func (c *Client) reconnect(ctx context.Context) {
c.heartbeatTicker = nil
}
+ // Swap the stop channel under the lock so subsequent callers see the new one, then
+ // release the lock and close the old channel from outside the critical section.
+ // This is what allows the heartbeat goroutine to exit via <-stopCh BEFORE we wait on
+ // heartbeatWg; closing stopCh AFTER the wait (the previous behavior) deadlocks any
+ // heartbeat goroutine that is parked on the select.
+ oldStopCh := c.stopCh
+ c.stopCh = make(chan struct{})
c.streamsMu.Unlock()
- c.heartbeatWg.Wait()
- c.streamsMu.Lock()
+ safeClose(oldStopCh)
- if !c.disconnected {
- close(c.stopCh)
+ if !waitWaitGroupWithTimeout(&c.heartbeatWg, heartbeatWaitTimeout) {
+ c.logger.Warn().Msg("Heartbeat goroutine did not exit in time during reconnect, proceeding anyway")
}
- c.stopCh = make(chan struct{})
+
+ c.streamsMu.Lock()
if c.registrationStream != nil {
_ = c.registrationStream.CloseSend()
c.registrationStream = nil
diff --git a/fodc/agent/internal/proxy/client_blocking_send_test.go b/fodc/agent/internal/proxy/client_blocking_send_test.go
new file mode 100644
index 000000000..203739a67
--- /dev/null
+++ b/fodc/agent/internal/proxy/client_blocking_send_test.go
@@ -0,0 +1,151 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 proxy
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/fodc/agent/internal/flightrecorder"
+)
+
+// newBlockingMockRegisterAgentClient returns a mock whose Send blocks until blockCh is
+// closed, mimicking a gRPC client stream that received GOAWAY but whose underlying TCP
+// write queue is no longer being drained. The entered channel is closed once the first
+// time Send is called so tests can synchronize with the heartbeat goroutine reaching the
+// blocking call.
+func newBlockingMockRegisterAgentClient(ctx context.Context) *mockRegisterAgentClient {
+ mock := newMockRegisterAgentClient(ctx)
+ mock.sendBlock = make(chan struct{})
+ mock.sendEntered = make(chan struct{})
+ return mock
+}
+
+// TestProxyClient_SendHeartbeat_RespectsCtxOnBlockingSend reproduces the bug where
+// SendHeartbeat discards its ctx parameter. With a registration stream whose Send blocks
+// indefinitely, the caller's context timeout has no effect: SendHeartbeat hangs forever
+// instead of returning when the deadline elapses.
+//
+// Pre-fix behavior: SendHeartbeat blocks far beyond the 100ms deadline; the test reaches
+// its 2s ceiling and calls t.Fatal.
+// Post-fix behavior: SendHeartbeat returns shortly after the 100ms deadline with an error
+// that wraps context.DeadlineExceeded.
+func TestProxyClient_SendHeartbeat_RespectsCtxOnBlockingSend(t *testing.T) {
+ testLogger := initTestLogger(t)
+ fr := flightrecorder.NewFlightRecorder(1000000)
+ pc := NewProxyClient(
+ "localhost:8080", "datanode-hot", "192.168.1.1", []string{"data"},
+ nil, 5*time.Second, 10*time.Second, fr, nil, nil, testLogger,
+ )
+
+ streamCtx, streamCancel := context.WithCancel(context.Background())
+ defer streamCancel()
+ stream := newBlockingMockRegisterAgentClient(streamCtx)
+ defer close(stream.sendBlock)
+
+ pc.streamsMu.Lock()
+ pc.registrationStream = stream
+ pc.streamsMu.Unlock()
+
+ callCtx, callCancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
+ defer callCancel()
+
+ done := make(chan error, 1)
+ go func() { done <- pc.SendHeartbeat(callCtx) }()
+
+ select {
+ case err := <-done:
+ require.Error(t, err, "SendHeartbeat must return an error when its ctx times out")
+ assert.Truef(t,
+ errors.Is(err, context.DeadlineExceeded),
+ "expected error wrapping context.DeadlineExceeded, got: %v", err,
+ )
+ case <-time.After(2 * time.Second):
+ t.Fatal("SendHeartbeat ignored its context deadline: blocked Send hangs forever " +
+ "(root cause: SendHeartbeat declares ctx as `_` and never applies it to Send)")
+ }
+}
+
+// TestProxyClient_reconnect_DeadlocksWhenHeartbeatBlockedOnSend reproduces the central bug
+// from the production incident: when a heartbeat goroutine is in the middle of
+// registrationStream.Send and that Send is stuck (e.g. proxy entered graceful_stop and the
+// stream's underlying TCP write queue is no longer being drained), the reconnect path's
+// c.heartbeatWg.Wait() never returns. Combined with the fact that reconnect closes stopCh
+// AFTER Wait rather than before, the heartbeat goroutine has no way out: its Send is stuck,
+// ctx is alive, stopCh is still open, ticker is stopped. The result in production is the
+// agent silently turning into a zombie that no longer talks to the proxy.
+//
+// Pre-fix behavior: pc.reconnect blocks indefinitely; the test reaches its 2s ceiling and
+// calls t.Fatal (heartbeatWaitTimeout is overridden to 200ms below to keep the post-fix
+// assertion fast; without the fix the call still hangs because Wait has no timeout at all).
+// Post-fix behavior: pc.reconnect returns within roughly the overridden heartbeatWaitTimeout.
+func TestProxyClient_reconnect_DeadlocksWhenHeartbeatBlockedOnSend(t *testing.T) {
+ originalTimeout := heartbeatWaitTimeout
+ heartbeatWaitTimeout = 200 * time.Millisecond
+ defer func() { heartbeatWaitTimeout = originalTimeout }()
+
+ testLogger := initTestLogger(t)
+ fr := flightrecorder.NewFlightRecorder(1000000)
+ pc := NewProxyClient(
+ "localhost:8080", "datanode-hot", "192.168.1.1", []string{"data"},
+ nil, 10*time.Millisecond, 100*time.Millisecond, fr, nil, nil, testLogger,
+ )
+
+ bgCtx, bgCancel := context.WithCancel(context.Background())
+ defer bgCancel()
+
+ streamCtx, streamCancel := context.WithCancel(context.Background())
+ defer streamCancel()
+ stream := newBlockingMockRegisterAgentClient(streamCtx)
+ defer close(stream.sendBlock)
+
+ pc.streamsMu.Lock()
+ pc.registrationStream = stream
+ pc.streamsMu.Unlock()
+
+ pc.connManager.start(bgCtx)
+ defer pc.connManager.stop()
+
+ pc.startHeartbeat(bgCtx)
+
+ select {
+ case <-stream.sendEntered:
+ case <-time.After(2 * time.Second):
+ t.Fatal("heartbeat goroutine never entered Send within 2s; cannot reproduce blocking-Send scenario")
+ }
+
+ done := make(chan struct{})
+ go func() {
+ pc.reconnect(bgCtx)
+ close(done)
+ }()
+
+ select {
+ case <-done:
+ case <-time.After(2 * time.Second):
+ t.Fatal("reconnect deadlocked: heartbeatWg.Wait() never returned while a heartbeat " +
+ "goroutine was blocked inside registrationStream.Send (root causes: SendHeartbeat " +
+ "ignores ctx + reconnect waits on heartbeatWg without a bounded timeout + reconnect " +
+ "closes stopCh AFTER Wait instead of before)")
+ }
+}
diff --git a/fodc/agent/internal/proxy/client_test.go b/fodc/agent/internal/proxy/client_test.go
index b37229ef5..0c5346c3d 100644
--- a/fodc/agent/internal/proxy/client_test.go
+++ b/fodc/agent/internal/proxy/client_test.go
@@ -85,13 +85,20 @@ func (m *mockFODCServiceClient) StreamLifecycle(_ context.Context, _ ...grpc.Cal
}
// mockRegisterAgentClient implements fodcv1.FODCService_RegisterAgentClient for testing.
+// When sendBlock is non-nil, Send blocks on it until the channel is closed (used by tests
+// that reproduce the wedged-Send scenario). When sendEntered is non-nil, it is closed once
+// the first time Send is invoked, letting tests synchronize with the goroutine reaching
+// the blocking call.
type mockRegisterAgentClient struct {
ctx context.Context
recvChan chan *fodcv1.RegisterAgentResponse
+ sendBlock chan struct{}
+ sendEntered chan struct{}
sendErr error
recvErr error
sentRequests []*fodcv1.RegisterAgentRequest
recvResponses []*fodcv1.RegisterAgentResponse
+ enterOnce sync.Once
mu sync.RWMutex
}
@@ -105,6 +112,12 @@ func newMockRegisterAgentClient(ctx context.Context) *mockRegisterAgentClient {
}
func (m *mockRegisterAgentClient) Send(req *fodcv1.RegisterAgentRequest) error {
+ if m.sendEntered != nil {
+ m.enterOnce.Do(func() { close(m.sendEntered) })
+ }
+ if m.sendBlock != nil {
+ <-m.sendBlock
+ }
m.mu.Lock()
defer m.mu.Unlock()
if m.sendErr != nil {
diff --git a/fodc/agent/internal/proxy/wait.go b/fodc/agent/internal/proxy/wait.go
new file mode 100644
index 000000000..3e578f36d
--- /dev/null
+++ b/fodc/agent/internal/proxy/wait.go
@@ -0,0 +1,69 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 proxy
+
+import (
+ "sync"
+ "time"
+)
+
+// heartbeatWaitTimeout bounds how long reconnect, cleanupStreams and Disconnect will wait
+// for the heartbeat goroutine to exit. It is a package-level variable rather than a const
+// so that tests reproducing blocking-Send scenarios can shorten it without dragging real
+// production wait times down. The default of 10s tracks the conn_manager's own 5s heartbeat
+// check timeout plus enough slack for one Send round-trip.
+var heartbeatWaitTimeout = 10 * time.Second
+
+// waitWaitGroupWithTimeout waits for wg to reach zero, but no longer than d. It returns
+// true when wg completed before the timeout fired, false when the timeout fired first.
+//
+// On the timeout branch the inner waiter goroutine remains parked on wg.Wait() until wg
+// eventually reaches zero (sync.WaitGroup offers no cancellation). Callers should ensure
+// that whatever holds the wg counter up will eventually be released; in this package the
+// reconnect / cleanupStreams / Disconnect paths close the registration stream right after
+// calling this helper, which unblocks any Send the heartbeat goroutine is parked in and
+// lets it run to completion.
+func waitWaitGroupWithTimeout(wg *sync.WaitGroup, d time.Duration) bool {
+ done := make(chan struct{})
+ go func() {
+ wg.Wait()
+ close(done)
+ }()
+ timer := time.NewTimer(d)
+ defer timer.Stop()
+ select {
+ case <-done:
+ return true
+ case <-timer.C:
+ return false
+ }
+}
+
+// safeClose closes ch if it is non-nil and has not already been closed. The caller is
+// responsible for ensuring no other goroutine closes ch concurrently; this helper only
+// guards against the "already closed by an earlier call from this same path" case.
+func safeClose(ch chan struct{}) {
+ if ch == nil {
+ return
+ }
+ select {
+ case <-ch:
+ default:
+ close(ch)
+ }
+}
diff --git a/fodc/agent/internal/proxy/wait_test.go b/fodc/agent/internal/proxy/wait_test.go
new file mode 100644
index 000000000..9c4ee0c1a
--- /dev/null
+++ b/fodc/agent/internal/proxy/wait_test.go
@@ -0,0 +1,61 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 proxy
+
+import (
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestWaitWaitGroupWithTimeout_CompletesBeforeTimeout(t *testing.T) {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ go func() {
+ time.Sleep(20 * time.Millisecond)
+ wg.Done()
+ }()
+
+ completed := waitWaitGroupWithTimeout(&wg, 500*time.Millisecond)
+
+ assert.True(t, completed, "expected helper to report completion before the timeout fired")
+}
+
+func TestWaitWaitGroupWithTimeout_TimesOut(t *testing.T) {
+ var wg sync.WaitGroup
+ wg.Add(1)
+ defer wg.Done()
+
+ start := time.Now()
+ completed := waitWaitGroupWithTimeout(&wg, 50*time.Millisecond)
+ elapsed := time.Since(start)
+
+ assert.False(t, completed, "expected helper to report timeout when wg never reaches zero")
+ assert.GreaterOrEqual(t, elapsed, 50*time.Millisecond, "helper returned earlier than the configured timeout")
+ assert.Less(t, elapsed, 500*time.Millisecond, "helper waited significantly past the configured timeout")
+}
+
+func TestWaitWaitGroupWithTimeout_AlreadyDone(t *testing.T) {
+ var wg sync.WaitGroup
+
+ completed := waitWaitGroupWithTimeout(&wg, 100*time.Millisecond)
+
+ assert.True(t, completed, "expected helper to return immediately for an already-zero WaitGroup")
+}
diff --git a/fodc/proxy/internal/api/server.go b/fodc/proxy/internal/api/server.go
index d823752ce..3ff5de920 100644
--- a/fodc/proxy/internal/api/server.go
+++ b/fodc/proxy/internal/api/server.go
@@ -476,6 +476,15 @@ type timeSeriesMetric struct {
}
// handleClusterLifecycle handles GET /cluster/lifecycle endpoint.
+//
+// The handler adds an "error" field to the response body whenever the proxy could not
+// gather any lifecycle data — that is, when no agent was registered (Total == 0), no
+// registered agent supported the lifecycle stream (Requested == 0), or none of the
+// requested agents responded within the collection window (Responded == 0). The error
+// message describes which of these three sub-cases occurred so callers can distinguish an
+// infrastructure-layer outage (e.g. proxy pod restart with every agent failing to
+// reconnect) from a cluster that genuinely has no groups. The HTTP status stays 200 in
+// every case; the distinguishing signal is in the body, not in the status code.
func (s *Server) handleClusterLifecycle(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
@@ -485,10 +494,27 @@ func (s *Server) handleClusterLifecycle(w http.ResponseWriter, r *http.Request)
http.Error(w, "Lifecycle manager not available", http.StatusServiceUnavailable)
return
}
- lifecycleData := s.lifecycleManager.CollectLifecycle(r.Context())
+ lifecycleData, agentSummary := s.lifecycleManager.CollectLifecycle(r.Context())
+
+ body := map[string]interface{}{
+ "groups": lifecycleData.Groups,
+ "lifecycle_statuses": lifecycleData.LifecycleStatuses,
+ "agent_summary": agentSummary,
+ }
+ switch {
+ case agentSummary.Total == 0:
+ body["error"] = "FODC unavailable: no agents registered"
+ case agentSummary.Requested == 0:
+ body["error"] = fmt.Sprintf("FODC unavailable: 0/%d registered agents support the lifecycle stream",
+ agentSummary.Total)
+ case agentSummary.Responded == 0:
+ body["error"] = fmt.Sprintf("FODC unavailable: 0/%d requested agents responded",
+ agentSummary.Requested)
+ }
+
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
- if encodeErr := json.NewEncoder(w).Encode(lifecycleData); encodeErr != nil {
+ if encodeErr := json.NewEncoder(w).Encode(body); encodeErr != nil {
s.logger.Error().Err(encodeErr).Msg("Failed to encode lifecycle response")
}
}
diff --git a/fodc/proxy/internal/api/server_test.go b/fodc/proxy/internal/api/server_test.go
index e6182bb73..b39d8c609 100644
--- a/fodc/proxy/internal/api/server_test.go
+++ b/fodc/proxy/internal/api/server_test.go
@@ -781,3 +781,55 @@ func TestHandleClusterLifecycle_NoLifecycleManager(t *testing.T) {
assert.Equal(t, http.StatusServiceUnavailable, resp.Code)
}
+
+// TestHandleClusterLifecycle_NoAgents_FlagsErrorInBody reproduces the bug where the proxy
+// silently returns 200 + an empty payload when no agents are registered. This makes the
+// caller (e.g. the SRE inspection agent) unable to distinguish two semantically very
+// different states:
+// - the BanyanDB cluster genuinely has no groups (a real cluster-side problem); or
+// - the FODC proxy lost all of its agents (an infrastructure-layer outage caused by a
+// proxy pod restart, every agent stuck on reconnect, etc.).
+//
+// In production the second case is what triggered the lifecycle critical alert, but the
+// HTTP layer presented it identically to the first.
+//
+// Pre-fix behavior: handleClusterLifecycle writes 200 with body
+// `{"groups":[],"lifecycle_statuses":[]}` and no way to tell the two cases apart.
+// Post-fix behavior: HTTP status is still 200, but the body now carries an "error" field
+// when agent_summary reports total=0 or responded=0, plus the agent_summary itself.
+func TestHandleClusterLifecycle_NoAgents_FlagsErrorInBody(t *testing.T) {
+ initTestLogger(t)
+ testLogger := logger.GetLogger("test", "api")
+ testRegistry := registry.NewAgentRegistry(testLogger, 5*time.Second, 10*time.Second, 100)
+ defer testRegistry.Stop()
+ mockSender := &mockRequestSender{}
+ aggregator := metrics.NewAggregator(testRegistry, mockSender, testLogger)
+
+ mockLifecycleRequester := &mockLifecycleDataRequester{
+ dataByAgent: make(map[string]*fodcv1.LifecycleData),
+ podByAgent: make(map[string]string),
+ }
+ lifecycleMgr := lifecycle.NewManager(testRegistry, mockLifecycleRequester, testLogger)
+ mockLifecycleRequester.lifecycleMgr = lifecycleMgr
+
+ server := NewServer(aggregator, nil, lifecycleMgr, testRegistry, testLogger)
+
+ req := httptest.NewRequest(http.MethodGet, "/cluster/lifecycle", nil)
+ resp := httptest.NewRecorder()
+ server.handleClusterLifecycle(resp, req)
+
+ assert.Equal(t, http.StatusOK, resp.Code, "status code stays 200 even when zero agents are registered")
+
+ var body map[string]interface{}
+ require.NoError(t, json.NewDecoder(resp.Body).Decode(&body))
+
+ errMsg, hasError := body["error"]
+ assert.Truef(t, hasError, "body must include an error field when zero agents are registered, got: %v", body)
+ assert.Containsf(t, errMsg, "FODC unavailable",
+ "error field should describe the infrastructure-layer failure, got: %v", errMsg)
+
+ summary, hasSummary := body["agent_summary"].(map[string]interface{})
+ require.Truef(t, hasSummary, "body must include agent_summary, got: %v", body)
+ assert.EqualValues(t, 0, summary["total"], "agent_summary.total should be 0 when no agents are registered")
+ assert.EqualValues(t, 0, summary["responded"], "agent_summary.responded should be 0 when no agents are registered")
+}
diff --git a/fodc/proxy/internal/lifecycle/manager.go b/fodc/proxy/internal/lifecycle/manager.go
index 66cb9ce81..207354730 100644
--- a/fodc/proxy/internal/lifecycle/manager.go
+++ b/fodc/proxy/internal/lifecycle/manager.go
@@ -42,6 +42,18 @@ type InspectionResult struct {
LifecycleStatuses []*PodLifecycleStatus `json:"lifecycle_statuses"`
}
+// AgentSummary describes how many agents the most recent CollectLifecycle invocation saw,
+// requested data from, and actually got data back from. It lets callers tell apart "the
+// cluster has nothing to report" (cluster-side empty) from "the proxy could not reach any
+// agent" (infrastructure-side empty), which look identical when only the InspectionResult
+// is observed.
+type AgentSummary struct {
+ Total int `json:"total"`
+ Requested int `json:"requested"`
+ Responded int `json:"responded"`
+ NotResponded int `json:"not_responded"`
+}
+
func emptyResult() *InspectionResult {
return &InspectionResult{
Groups: make([]*fodcv1.GroupLifecycleInfo, 0),
@@ -106,67 +118,95 @@ func (m *Manager) UpdateLifecycle(agentID, podName string, data *fodcv1.Lifecycl
}
}
-// CollectLifecycle requests and collects lifecycle data from all agents.
-func (m *Manager) CollectLifecycle(ctx context.Context) *InspectionResult {
+func (m *Manager) registerSession(agentID string, collectChs map[string]chan *agentLifecycleData) {
+ collectCh := make(chan *agentLifecycleData, 1)
+ collectChs[agentID] = collectCh
+ m.collectingMu.Lock()
+ m.collecting[agentID] = collectCh
+ m.collectingMu.Unlock()
+}
+
+func (m *Manager) unregisterSession(agentID string, collectChs map[string]chan *agentLifecycleData) {
+ m.collectingMu.Lock()
+ if ch, exists := m.collecting[agentID]; exists {
+ close(ch)
+ delete(m.collecting, agentID)
+ }
+ m.collectingMu.Unlock()
+ delete(collectChs, agentID)
+}
+
+// CollectLifecycle requests and collects lifecycle data from all agents and returns
+// both the aggregated result and the agent summary captured atomically during the same
+// invocation. Returning the summary as a second value (rather than via a separate
+// "LastSummary" accessor) avoids a read-after-write race between the result and the
+// summary when concurrent HTTP requests trigger overlapping collections.
+func (m *Manager) CollectLifecycle(ctx context.Context) (*InspectionResult, AgentSummary) {
m.collectingOp.Lock()
defer m.collectingOp.Unlock()
- if m.registry == nil {
- m.log.Info().Msg("CollectLifecycle: registry is nil, returning empty")
- return emptyResult()
+ summary := AgentSummary{}
+
+ if m.registry == nil || m.grpcService == nil {
+ m.log.Info().Msg("CollectLifecycle: registry or grpcService is nil, returning empty")
+ return emptyResult(), summary
}
agents := m.registry.ListAgents()
+ summary.Total = len(agents)
if len(agents) == 0 {
m.log.Info().Msg("CollectLifecycle: no agents registered, returning empty")
- return emptyResult()
+ return emptyResult(), summary
}
m.log.Info().Int("agent_count", len(agents)).Msg("CollectLifecycle: starting collection")
collectChs := make(map[string]chan *agentLifecycleData)
+ defer m.cleanupSessions(collectChs)
- defer func() {
- m.collectingMu.Lock()
- for agentID, collectCh := range collectChs {
- close(collectCh)
- delete(m.collecting, agentID)
- }
- m.collectingMu.Unlock()
- }()
+ summary.Requested = m.requestAllAgents(ctx, agents, collectChs)
+ m.log.Info().Int("requested", summary.Requested).Int("waiting_for", len(collectChs)).
+ Msg("CollectLifecycle: requests sent, waiting for responses")
+
+ allData := m.waitForResponses(ctx, collectChs)
+ summary.Responded = len(allData)
+ if summary.Requested >= summary.Responded {
+ summary.NotResponded = summary.Requested - summary.Responded
+ }
+ m.log.Info().Int("responses_with_data", len(allData)).
+ Msg("CollectLifecycle: all responses collected, aggregating")
+ return m.buildInspectionResult(allData), summary
+}
+
+func (m *Manager) requestAllAgents(ctx context.Context, agents []*registry.AgentInfo,
+ collectChs map[string]chan *agentLifecycleData,
+) int {
requestedCount := 0
for _, agentInfo := range agents {
select {
case <-ctx.Done():
m.log.Info().Msg("CollectLifecycle: context canceled during request phase")
- return emptyResult()
+ return requestedCount
default:
}
- if m.grpcService == nil {
- m.log.Info().Str("agent_id", agentInfo.AgentID).Msg("CollectLifecycle: grpcService is nil, skipping request")
- continue
- }
+ m.registerSession(agentInfo.AgentID, collectChs)
if err := m.grpcService.RequestLifecycleData(agentInfo.AgentID); err != nil {
m.log.Info().Err(err).
Str("agent_id", agentInfo.AgentID).
Msg("Agent does not support lifecycle stream, skipping")
+ m.unregisterSession(agentInfo.AgentID, collectChs)
continue
}
- collectCh := make(chan *agentLifecycleData, 1)
- collectChs[agentInfo.AgentID] = collectCh
- m.collectingMu.Lock()
- m.collecting[agentInfo.AgentID] = collectCh
- m.collectingMu.Unlock()
requestedCount++
}
+ return requestedCount
+}
- m.log.Info().Int("requested", requestedCount).Int("waiting_for", len(collectChs)).Msg("CollectLifecycle: requests sent, waiting for responses")
-
+func (m *Manager) waitForResponses(ctx context.Context, collectChs map[string]chan *agentLifecycleData) []*agentLifecycleData {
allData := make([]*agentLifecycleData, 0, len(collectChs))
var dataMu sync.Mutex
var wg sync.WaitGroup
-
for agentID, collectCh := range collectChs {
wg.Add(1)
go func(id string, ch chan *agentLifecycleData) {
@@ -175,9 +215,7 @@ func (m *Manager) CollectLifecycle(ctx context.Context) *InspectionResult {
defer agentCancel()
select {
case <-agentCtx.Done():
- m.log.Warn().
- Str("agent_id", id).
- Msg("Timeout waiting for lifecycle data from agent")
+ m.log.Warn().Str("agent_id", id).Msg("Timeout waiting for lifecycle data from agent")
case data := <-ch:
if data != nil {
m.log.Info().
@@ -189,17 +227,21 @@ func (m *Manager) CollectLifecycle(ctx context.Context) *InspectionResult {
dataMu.Lock()
allData = append(allData, data)
dataMu.Unlock()
- } else {
- m.log.Info().Str("agent_id", id).Msg("CollectLifecycle: received nil data from agent")
}
}
}(agentID, collectCh)
}
-
wg.Wait()
- m.log.Info().Int("responses_with_data", len(allData)).Msg("CollectLifecycle: all responses collected, aggregating")
+ return allData
+}
- return m.buildInspectionResult(allData)
+func (m *Manager) cleanupSessions(collectChs map[string]chan *agentLifecycleData) {
+ m.collectingMu.Lock()
+ for agentID, collectCh := range collectChs {
+ close(collectCh)
+ delete(m.collecting, agentID)
+ }
+ m.collectingMu.Unlock()
}
func (m *Manager) buildInspectionResult(allData []*agentLifecycleData) *InspectionResult {
diff --git a/fodc/proxy/internal/lifecycle/manager_test.go b/fodc/proxy/internal/lifecycle/manager_test.go
index 7df3a2811..55a76f166 100644
--- a/fodc/proxy/internal/lifecycle/manager_test.go
+++ b/fodc/proxy/internal/lifecycle/manager_test.go
@@ -115,21 +115,24 @@ func TestManager_UpdateLifecycle_NoActiveCollection(t *testing.T) {
func TestManager_CollectLifecycle_NoAgents(t *testing.T) {
log := initTestLogger(t)
testRegistry := registry.NewAgentRegistry(log, 5*time.Second, 10*time.Second, 100)
+ defer testRegistry.Stop()
mgr := NewManager(testRegistry, nil, log)
- result := mgr.CollectLifecycle(context.Background())
+ result, summary := mgr.CollectLifecycle(context.Background())
require.NotNil(t, result)
assert.Empty(t, result.Groups)
assert.Empty(t, result.LifecycleStatuses)
+ assert.Equal(t, AgentSummary{}, summary, "summary must be zero when no agents are registered")
}
func TestManager_CollectLifecycle_NilRegistry(t *testing.T) {
log := initTestLogger(t)
mgr := NewManager(nil, nil, log)
- result := mgr.CollectLifecycle(context.Background())
+ result, summary := mgr.CollectLifecycle(context.Background())
require.NotNil(t, result)
assert.Empty(t, result.Groups)
+ assert.Equal(t, AgentSummary{}, summary, "summary must be zero when registry is nil")
}
func TestManager_CollectLifecycle_MultipleAgents(t *testing.T) {
@@ -169,9 +172,12 @@ func TestManager_CollectLifecycle_MultipleAgents(t *testing.T) {
},
}
+ var capturedSummary AgentSummary
done := make(chan *InspectionResult)
go func() {
- done <- mgr.CollectLifecycle(ctx)
+ result, summary := mgr.CollectLifecycle(ctx)
+ capturedSummary = summary
+ done <- result
}()
time.Sleep(50 * time.Millisecond)
@@ -194,6 +200,11 @@ func TestManager_CollectLifecycle_MultipleAgents(t *testing.T) {
podNames := []string{result.LifecycleStatuses[0].PodName, result.LifecycleStatuses[1].PodName}
assert.Contains(t, podNames, "pod-1")
assert.Contains(t, podNames, "pod-2")
+ // Verify summary reflects what actually happened
+ assert.Equal(t, 2, capturedSummary.Total, "summary.Total should match registered agents")
+ assert.Equal(t, 2, capturedSummary.Requested, "summary.Requested should match agents that accepted the request")
+ assert.Equal(t, 2, capturedSummary.Responded, "summary.Responded should match agents that returned data")
+ assert.Equal(t, 0, capturedSummary.NotResponded, "summary.NotResponded should be zero when all agents respond")
case <-time.After(5 * time.Second):
t.Fatal("Collection timed out")
@@ -219,7 +230,7 @@ func TestManager_CollectLifecycle_RequestError(t *testing.T) {
mockSender.SetRequestError(agentID, fmt.Errorf("connection refused"))
- result := mgr.CollectLifecycle(ctx)
+ result, _ := mgr.CollectLifecycle(ctx)
require.NotNil(t, result)
// Should return empty data since request failed
assert.Empty(t, result.Groups)
@@ -298,7 +309,10 @@ func TestManager_CollectLifecycle_WithGroups(t *testing.T) {
require.NoError(t, err)
done := make(chan *InspectionResult)
- go func() { done <- mgr.CollectLifecycle(ctx) }()
+ go func() {
+ result, _ := mgr.CollectLifecycle(ctx)
+ done <- result
+ }()
time.Sleep(50 * time.Millisecond)
mgr.UpdateLifecycle(agentID, "liaison-pod", &fodcv1.LifecycleData{
@@ -339,7 +353,10 @@ func TestManager_CollectLifecycle_GroupsFromFirstAgent(t *testing.T) {
require.NoError(t, err)
done := make(chan *InspectionResult)
- go func() { done <- mgr.CollectLifecycle(ctx) }()
+ go func() {
+ result, _ := mgr.CollectLifecycle(ctx)
+ done <- result
+ }()
time.Sleep(50 * time.Millisecond)
mgr.UpdateLifecycle(agentID1, "liaison-1", &fodcv1.LifecycleData{
@@ -374,6 +391,6 @@ func TestManager_CollectLifecycle_ContextCanceled(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // Cancel immediately
- result := mgr.CollectLifecycle(ctx)
+ result, _ := mgr.CollectLifecycle(ctx)
require.NotNil(t, result)
}
(skywalking-banyandb) 03/24: fix take snapshot error when no data in the segment (#1046)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit b21e06f3395a1cf0e92622d94ea4156c03ddb363
Author: mrproliu <74...@qq.com>
AuthorDate: Wed Apr 8 13:03:46 2026 +0800
fix take snapshot error when no data in the segment (#1046)
* fix take snapshot error when no data in the segment
---------
Co-authored-by: Gao Hongtao <ha...@gmail.com>
---
banyand/internal/storage/rotation_test.go | 21 +++++++++++++
banyand/internal/storage/storage.go | 4 ++-
banyand/internal/storage/tsdb.go | 31 ++++++++++++-------
banyand/internal/storage/tsdb_test.go | 50 +++++++++++++++++++++++++++++++
banyand/measure/snapshot.go | 13 +++++---
banyand/measure/snapshot_test.go | 40 +++++++++++++++++++++++++
banyand/stream/snapshot.go | 24 ++++++++-------
banyand/stream/snapshot_test.go | 41 +++++++++++++++++++++++++
banyand/trace/snapshot.go | 26 +++++++++-------
banyand/trace/snapshot_test.go | 46 ++++++++++++++++++++++++++++
10 files changed, 261 insertions(+), 35 deletions(-)
diff --git a/banyand/internal/storage/rotation_test.go b/banyand/internal/storage/rotation_test.go
index 5a605e67c..e9e003916 100644
--- a/banyand/internal/storage/rotation_test.go
+++ b/banyand/internal/storage/rotation_test.go
@@ -208,6 +208,27 @@ var MockTSTableCreator = func(_ fs.FileSystem, _ string, _ common.Position,
return &MockTSTable{}, nil
}
+type SnapshotMockTSTable struct {
+ timeRange timestamp.TimeRange
+}
+
+func (m *SnapshotMockTSTable) Close() error { return nil }
+
+func (m *SnapshotMockTSTable) Collect(_ Metrics) {}
+
+func (m *SnapshotMockTSTable) TakeFileSnapshot(_ string) (bool, error) {
+ if m.timeRange.Start.Equal(time.Unix(0, 0)) {
+ return false, ErrNoCurrentSnapshot
+ }
+ return true, nil
+}
+
+var SnapshotMockTSTableCreator = func(_ fs.FileSystem, _ string, _ common.Position,
+ _ *logger.Logger, timeRange timestamp.TimeRange, _, _ any,
+) (*SnapshotMockTSTable, error) {
+ return &SnapshotMockTSTable{timeRange: timeRange}, nil
+}
+
type MockMetrics struct{}
func (m *MockMetrics) DeleteAll() {}
diff --git a/banyand/internal/storage/storage.go b/banyand/internal/storage/storage.go
index 3b30bef84..799f42b82 100644
--- a/banyand/internal/storage/storage.go
+++ b/banyand/internal/storage/storage.go
@@ -64,7 +64,9 @@ const (
var (
// ErrUnknownShard indicates that the shard is not found.
ErrUnknownShard = errors.New("unknown shard")
- errOpenDatabase = errors.New("fails to open the database")
+ // ErrNoCurrentSnapshot is returned when a shard has no current snapshot available.
+ ErrNoCurrentSnapshot = errors.New("no current snapshot available")
+ errOpenDatabase = errors.New("fails to open the database")
lfs = fs.NewLocalFileSystemWithLogger(logger.GetLogger("storage"))
)
diff --git a/banyand/internal/storage/tsdb.go b/banyand/internal/storage/tsdb.go
index 6cc5c5369..16a9c387f 100644
--- a/banyand/internal/storage/tsdb.go
+++ b/banyand/internal/storage/tsdb.go
@@ -262,14 +262,14 @@ func (d *database[T, O]) UpdateOptions(resourceOpts *commonv1.ResourceOpts) {
d.segmentController.updateOptions(resourceOpts)
}
-func (d *database[T, O]) TakeFileSnapshot(dst string) (bool, error) {
+func (d *database[T, O]) TakeFileSnapshot(dst string) (success bool, err error) {
if d.closed.Load() {
return false, errors.New("database is closed")
}
- segments, err := d.segmentController.segments(true)
- if err != nil {
- return false, errors.Wrap(err, "failed to get segments")
+ segments, segErr := d.segmentController.segments(true)
+ if segErr != nil {
+ return false, errors.Wrap(segErr, "failed to get segments")
}
defer func() {
for _, seg := range segments {
@@ -281,6 +281,12 @@ func (d *database[T, O]) TakeFileSnapshot(dst string) (bool, error) {
return false, nil
}
+ defer func() {
+ if err != nil {
+ d.lfs.MustRMAll(dst)
+ }
+ }()
+
log.Info().Int("segment_count", len(segments)).Str("db_location", d.location).
Msgf("taking file snapshot for %s", dst)
for _, seg := range segments {
@@ -290,14 +296,14 @@ func (d *database[T, O]) TakeFileSnapshot(dst string) (bool, error) {
metadataSrc := filepath.Join(seg.location, metadataFilename)
metadataDest := filepath.Join(segPath, metadataFilename)
- if err := d.lfs.CreateHardLink(metadataSrc, metadataDest, nil); err != nil {
- return false, errors.Wrapf(err, "failed to snapshot metadata for segment %s", segDir)
+ if linkErr := d.lfs.CreateHardLink(metadataSrc, metadataDest, nil); linkErr != nil {
+ return false, errors.Wrapf(linkErr, "failed to snapshot metadata for segment %s", segDir)
}
indexPath := filepath.Join(segPath, seriesIndexDirName)
d.lfs.MkdirIfNotExist(indexPath, DirPerm)
- if err := seg.index.store.TakeFileSnapshot(indexPath); err != nil {
- return false, errors.Wrapf(err, "failed to snapshot index for segment %s", segDir)
+ if indexErr := seg.index.store.TakeFileSnapshot(indexPath); indexErr != nil {
+ return false, errors.Wrapf(indexErr, "failed to snapshot index for segment %s", segDir)
}
sLst := seg.sLst.Load()
@@ -308,8 +314,13 @@ func (d *database[T, O]) TakeFileSnapshot(dst string) (bool, error) {
shardDir := filepath.Base(shard.location)
shardPath := filepath.Join(segPath, shardDir)
d.lfs.MkdirIfNotExist(shardPath, DirPerm)
- if _, err := shard.table.TakeFileSnapshot(shardPath); err != nil {
- return false, errors.Wrapf(err, "failed to snapshot shard %s in segment %s", shardDir, segDir)
+ if _, shardErr := shard.table.TakeFileSnapshot(shardPath); shardErr != nil {
+ if errors.Is(shardErr, ErrNoCurrentSnapshot) {
+ log.Debug().Str("shard", shardDir).Str("segment", segDir).
+ Msg("skipping empty shard snapshot")
+ continue
+ }
+ return false, errors.Wrapf(shardErr, "failed to snapshot shard %s in segment %s", shardDir, segDir)
}
}
}
diff --git a/banyand/internal/storage/tsdb_test.go b/banyand/internal/storage/tsdb_test.go
index d3b84e889..08b285d7a 100644
--- a/banyand/internal/storage/tsdb_test.go
+++ b/banyand/internal/storage/tsdb_test.go
@@ -308,6 +308,56 @@ func TestTakeFileSnapshot(t *testing.T) {
require.NoError(t, tsdb.Close())
})
+
+ t.Run("Take snapshot skips shard with no current snapshot", func(t *testing.T) {
+ dir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ snapshotDir := filepath.Join(dir, "snapshot")
+
+ opts := TSDBOpts[*SnapshotMockTSTable, any]{
+ Location: dir,
+ SegmentInterval: IntervalRule{Unit: DAY, Num: 1},
+ TTL: IntervalRule{Unit: DAY, Num: 7},
+ ShardNum: 1,
+ TSTableCreator: SnapshotMockTSTableCreator,
+ }
+
+ ctx := context.Background()
+ mc := timestamp.NewMockClock()
+ ts, err := time.ParseInLocation("2006-01-02 15:04:05", "2024-05-01 00:00:00", time.Local)
+ require.NoError(t, err)
+ mc.Set(ts)
+ ctx = timestamp.SetClock(ctx, mc)
+
+ serviceCache := NewServiceCache()
+ tsdb, err := OpenTSDB(ctx, opts, serviceCache, group)
+ require.NoError(t, err)
+ defer tsdb.Close()
+
+ normalSeg, err := tsdb.CreateSegmentIfNotExist(ts)
+ require.NoError(t, err)
+ normalSegLocation := normalSeg.(*segment[*SnapshotMockTSTable, any]).location
+ normalSeg.DecRef()
+
+ epochSeg, err := tsdb.CreateSegmentIfNotExist(time.Unix(0, 0))
+ require.NoError(t, err)
+ epochSegLocation := epochSeg.(*segment[*SnapshotMockTSTable, any]).location
+ epochSeg.DecRef()
+
+ created, snapshotErr := tsdb.TakeFileSnapshot(snapshotDir)
+ require.NoError(t, snapshotErr,
+ "snapshot should not fail due to empty shard in epoch segment")
+ require.True(t, created)
+
+ normalSegDir := filepath.Join(snapshotDir, filepath.Base(normalSegLocation))
+ require.DirExists(t, normalSegDir,
+ "normal segment should be present in snapshot")
+
+ epochSegDir := filepath.Join(snapshotDir, filepath.Base(epochSegLocation))
+ require.DirExists(t, epochSegDir,
+ "epoch segment directory should still be created")
+ })
}
func TestTSDBCollect(t *testing.T) {
diff --git a/banyand/measure/snapshot.go b/banyand/measure/snapshot.go
index 80c1df9fd..fa8323a94 100644
--- a/banyand/measure/snapshot.go
+++ b/banyand/measure/snapshot.go
@@ -153,12 +153,17 @@ func parseSnapshot(name string) (uint64, error) {
return parseEpoch(name[:16])
}
-func (tst *tsTable) TakeFileSnapshot(dst string) (bool, error) {
+func (tst *tsTable) TakeFileSnapshot(dst string) (success bool, err error) {
snapshot := tst.currentSnapshot()
if snapshot == nil {
- return false, fmt.Errorf("no current snapshot available")
+ return false, storage.ErrNoCurrentSnapshot
}
defer snapshot.decRef()
+ defer func() {
+ if err != nil {
+ tst.fileSystem.MustRMAll(dst)
+ }
+ }()
hasDiskParts := false
for _, pw := range snapshot.parts {
@@ -171,8 +176,8 @@ func (tst *tsTable) TakeFileSnapshot(dst string) (bool, error) {
srcPath := part.path
destPartPath := filepath.Join(dst, filepath.Base(srcPath))
- if err := tst.fileSystem.CreateHardLink(srcPath, destPartPath, nil); err != nil {
- return false, fmt.Errorf("failed to create snapshot for part %d: %w", part.partMetadata.ID, err)
+ if linkErr := tst.fileSystem.CreateHardLink(srcPath, destPartPath, nil); linkErr != nil {
+ return false, fmt.Errorf("failed to create snapshot for part %d: %w", part.partMetadata.ID, linkErr)
}
}
if !hasDiskParts {
diff --git a/banyand/measure/snapshot_test.go b/banyand/measure/snapshot_test.go
index dc8c51d6b..67aa4f896 100644
--- a/banyand/measure/snapshot_test.go
+++ b/banyand/measure/snapshot_test.go
@@ -812,3 +812,43 @@ func TestInitTSTableLoadsNewestWhenMultipleValid(t *testing.T) {
sort.Slice(snapshots, func(i, j int) bool { return snapshots[i] > snapshots[j] })
require.Equal(t, snapshots[0], epoch2, "must load newest valid snapshot")
}
+
+func TestTakeFileSnapshotEmptySegment(t *testing.T) {
+ fileSystem := fs.NewLocalFileSystem()
+
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, err := newTSTable(
+ fileSystem,
+ tabDir,
+ common.Position{},
+ logger.GetLogger("test"),
+ timestamp.TimeRange{},
+ option{
+ flushTimeout: 0,
+ mergePolicy: newDefaultMergePolicy(),
+ protector: protector.Nop{},
+ },
+ nil,
+ )
+ require.NoError(t, err)
+ defer tst.Close()
+
+ tst.Lock()
+ tst.snapshot = nil
+ tst.Unlock()
+
+ snapshotPath := filepath.Join(tmpPath, "snapshot")
+ fileSystem.MkdirIfNotExist(snapshotPath, 0o755)
+
+ created, err := tst.TakeFileSnapshot(snapshotPath)
+ require.ErrorIs(t, err, storage.ErrNoCurrentSnapshot)
+ assert.False(t, created)
+
+ entries := fileSystem.ReadDir(snapshotPath)
+ assert.Empty(t, entries, "no files or dirs should remain when snapshot is nil")
+}
diff --git a/banyand/stream/snapshot.go b/banyand/stream/snapshot.go
index 67f79f335..e3fd0d9ff 100644
--- a/banyand/stream/snapshot.go
+++ b/banyand/stream/snapshot.go
@@ -195,21 +195,25 @@ func parseSnapshot(name string) (uint64, error) {
return parseEpoch(name[:16])
}
-func (tst *tsTable) TakeFileSnapshot(dst string) (bool, error) {
+func (tst *tsTable) TakeFileSnapshot(dst string) (success bool, err error) {
if tst.index == nil {
return false, fmt.Errorf("cannot take file snapshot: index is not initialized for this tsTable")
}
- indexDir := filepath.Join(dst, filepath.Base(tst.index.location))
- tst.fileSystem.MkdirPanicIfExist(indexDir, storage.DirPerm)
- if err := tst.index.store.TakeFileSnapshot(indexDir); err != nil {
- return false, fmt.Errorf("failed to take file snapshot for index: %w", err)
- }
-
snapshot := tst.currentSnapshot()
if snapshot == nil {
- return false, fmt.Errorf("no current snapshot available")
+ return false, storage.ErrNoCurrentSnapshot
}
defer snapshot.decRef()
+ defer func() {
+ if err != nil {
+ tst.fileSystem.MustRMAll(dst)
+ }
+ }()
+ indexDir := filepath.Join(dst, filepath.Base(tst.index.location))
+ tst.fileSystem.MkdirPanicIfExist(indexDir, storage.DirPerm)
+ if indexErr := tst.index.store.TakeFileSnapshot(indexDir); indexErr != nil {
+ return false, fmt.Errorf("failed to take file snapshot for index: %w", indexErr)
+ }
hasDiskParts := false
for _, pw := range snapshot.parts {
@@ -221,8 +225,8 @@ func (tst *tsTable) TakeFileSnapshot(dst string) (bool, error) {
srcPath := part.path
destPartPath := filepath.Join(dst, filepath.Base(srcPath))
- if err := tst.fileSystem.CreateHardLink(srcPath, destPartPath, nil); err != nil {
- return false, fmt.Errorf("failed to create snapshot for part %d: %w", part.partMetadata.ID, err)
+ if linkErr := tst.fileSystem.CreateHardLink(srcPath, destPartPath, nil); linkErr != nil {
+ return false, fmt.Errorf("failed to create snapshot for part %d: %w", part.partMetadata.ID, linkErr)
}
hasDiskParts = true
}
diff --git a/banyand/stream/snapshot_test.go b/banyand/stream/snapshot_test.go
index bfbad9c79..d98560b19 100644
--- a/banyand/stream/snapshot_test.go
+++ b/banyand/stream/snapshot_test.go
@@ -27,6 +27,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/apache/skywalking-banyandb/api/common"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
"github.com/apache/skywalking-banyandb/banyand/protector"
"github.com/apache/skywalking-banyandb/pkg/bytes"
"github.com/apache/skywalking-banyandb/pkg/fs"
@@ -609,3 +610,43 @@ func TestTakeFileSnapshotNoDiskParts(t *testing.T) {
}
assert.True(t, hasIndex, "expected index directory in snapshot")
}
+
+func TestTakeFileSnapshotEmptySegment(t *testing.T) {
+ fileSystem := fs.NewLocalFileSystem()
+
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, err := newTSTable(
+ fileSystem,
+ tabDir,
+ common.Position{},
+ logger.GetLogger("test"),
+ timestamp.TimeRange{},
+ option{
+ flushTimeout: 0,
+ mergePolicy: newDefaultMergePolicy(),
+ protector: protector.Nop{},
+ },
+ nil,
+ )
+ require.NoError(t, err)
+ defer tst.Close()
+
+ tst.Lock()
+ tst.snapshot = nil
+ tst.Unlock()
+
+ snapshotPath := filepath.Join(tmpPath, "snapshot")
+ fileSystem.MkdirIfNotExist(snapshotPath, 0o755)
+
+ created, err := tst.TakeFileSnapshot(snapshotPath)
+ require.ErrorIs(t, err, storage.ErrNoCurrentSnapshot)
+ assert.False(t, created)
+
+ entries := fileSystem.ReadDir(snapshotPath)
+ assert.Empty(t, entries, "no files or dirs should remain when snapshot is nil")
+}
diff --git a/banyand/trace/snapshot.go b/banyand/trace/snapshot.go
index 9b8d3a485..6c120d7d3 100644
--- a/banyand/trace/snapshot.go
+++ b/banyand/trace/snapshot.go
@@ -217,19 +217,25 @@ func parseSnapshot(name string) (uint64, error) {
return parseEpoch(name[:16])
}
-func (tst *tsTable) TakeFileSnapshot(dst string) (bool, error) {
+func (tst *tsTable) TakeFileSnapshot(dst string) (success bool, err error) {
+ snapshot := tst.currentSnapshot()
+ if snapshot == nil {
+ return false, storage.ErrNoCurrentSnapshot
+ }
+ defer snapshot.decRef()
+ defer func() {
+ if err != nil {
+ tst.fileSystem.MustRMAll(dst)
+ }
+ }()
+
for k, v := range tst.sidxMap {
indexDir := filepath.Join(dst, sidxDirName, k)
tst.fileSystem.MkdirPanicIfExist(indexDir, storage.DirPerm)
- if err := v.TakeFileSnapshot(indexDir); err != nil {
- return false, fmt.Errorf("failed to take file snapshot for index, %s: %w", k, err)
+ if sidxErr := v.TakeFileSnapshot(indexDir); sidxErr != nil {
+ return false, fmt.Errorf("failed to take file snapshot for index, %s: %w", k, sidxErr)
}
}
- snapshot := tst.currentSnapshot()
- if snapshot == nil {
- return false, fmt.Errorf("no current snapshot available")
- }
- defer snapshot.decRef()
hasDiskParts := false
for _, pw := range snapshot.parts {
@@ -241,8 +247,8 @@ func (tst *tsTable) TakeFileSnapshot(dst string) (bool, error) {
srcPath := part.path
destPartPath := filepath.Join(dst, filepath.Base(srcPath))
- if err := tst.fileSystem.CreateHardLink(srcPath, destPartPath, nil); err != nil {
- return false, fmt.Errorf("failed to create snapshot for part %d: %w", part.partMetadata.ID, err)
+ if linkErr := tst.fileSystem.CreateHardLink(srcPath, destPartPath, nil); linkErr != nil {
+ return false, fmt.Errorf("failed to create snapshot for part %d: %w", part.partMetadata.ID, linkErr)
}
hasDiskParts = true
}
diff --git a/banyand/trace/snapshot_test.go b/banyand/trace/snapshot_test.go
index 9f3db9fcc..f8909c9dd 100644
--- a/banyand/trace/snapshot_test.go
+++ b/banyand/trace/snapshot_test.go
@@ -27,6 +27,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/apache/skywalking-banyandb/api/common"
+ "github.com/apache/skywalking-banyandb/banyand/internal/storage"
"github.com/apache/skywalking-banyandb/banyand/protector"
"github.com/apache/skywalking-banyandb/pkg/bytes"
"github.com/apache/skywalking-banyandb/pkg/convert"
@@ -738,3 +739,48 @@ func TestTakeFileSnapshotNoDiskPartsWithoutSidx(t *testing.T) {
require.NoError(t, err)
assert.False(t, created, "TakeFileSnapshot should return false when sidxMap is empty and no disk parts")
}
+
+func TestTakeFileSnapshotEmptySegment(t *testing.T) {
+ fileSystem := fs.NewLocalFileSystem()
+
+ tmpPath, deferFn := test.Space(require.New(t))
+ defer deferFn()
+
+ tabDir := filepath.Join(tmpPath, "tab")
+ fileSystem.MkdirPanicIfExist(tabDir, 0o755)
+
+ tst, err := newTSTable(
+ fileSystem,
+ tabDir,
+ common.Position{},
+ logger.GetLogger("test"),
+ timestamp.TimeRange{},
+ option{
+ flushTimeout: 0,
+ mergePolicy: newDefaultMergePolicy(),
+ protector: protector.Nop{},
+ },
+ nil,
+ )
+ require.NoError(t, err)
+ defer tst.Close()
+
+ // Populate sidxMap so the test guards against the prior bug where the
+ // sidxMap loop created sidx directories before the nil-snapshot check.
+ _, err = tst.getOrCreateSidx("test_sidx")
+ require.NoError(t, err)
+
+ tst.Lock()
+ tst.snapshot = nil
+ tst.Unlock()
+
+ snapshotPath := filepath.Join(tmpPath, "snapshot")
+ fileSystem.MkdirIfNotExist(snapshotPath, 0o755)
+
+ created, err := tst.TakeFileSnapshot(snapshotPath)
+ require.ErrorIs(t, err, storage.ErrNoCurrentSnapshot)
+ assert.False(t, created)
+
+ entries := fileSystem.ReadDir(snapshotPath)
+ assert.Empty(t, entries, "no files or dirs should remain when snapshot is nil")
+}
(skywalking-banyandb) 09/24: fix(handoff): prevent enqueuing parts for online nodes via shared LocateAll (#1065)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 6304de7b9c43e98fb4f6e868f457c45b68aed3a5
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Mon Apr 13 11:32:12 2026 +0800
fix(handoff): prevent enqueuing parts for online nodes via shared LocateAll (#1065)
---
banyand/liaison/grpc/node.go | 29 ++++++++++++++++
banyand/liaison/grpc/node_test.go | 11 ++++++
banyand/measure/metadata.go | 16 +++------
banyand/stream/metadata.go | 16 +++------
banyand/trace/handoff_controller.go | 10 +++---
banyand/trace/handoff_storage_test.go | 64 +++++++++++++++++++++++++++++++++++
banyand/trace/metadata.go | 16 +++------
banyand/trace/svc_liaison.go | 18 +---------
8 files changed, 121 insertions(+), 59 deletions(-)
diff --git a/banyand/liaison/grpc/node.go b/banyand/liaison/grpc/node.go
index 608799735..cc9d3ab0a 100644
--- a/banyand/liaison/grpc/node.go
+++ b/banyand/liaison/grpc/node.go
@@ -19,6 +19,7 @@ package grpc
import (
"fmt"
+ "sort"
"sync"
"github.com/pkg/errors"
@@ -39,7 +40,10 @@ var (
// NodeRegistry is for locating data node with group/name of the metadata
// together with the shardID calculated from the incoming data.
type NodeRegistry interface {
+ // Locate returns the data node assigned to the given (group, name, shardID, replicaID).
Locate(group, name string, shardID, replicaID uint32) (string, error)
+ // LocateAll returns all distinct data nodes owning the given shard across all replicas.
+ LocateAll(group string, shardID uint32, replicas int) ([]string, error)
fmt.Stringer
}
@@ -74,6 +78,26 @@ func (n *clusterNodeService) Locate(group, name string, shardID, replicaID uint3
return nodeID, nil
}
+func (n *clusterNodeService) LocateAll(group string, shardID uint32, replicas int) ([]string, error) {
+ if replicas < 1 {
+ return nil, fmt.Errorf("replicas must be >= 1, got %d", replicas)
+ }
+ nodeSet := make(map[string]struct{}, replicas)
+ for replica := 0; replica < replicas; replica++ {
+ nodeID, err := n.Locate(group, "", shardID, uint32(replica))
+ if err != nil {
+ return nil, errors.Wrapf(err, "fail to locate %s/%d/%d", group, shardID, replica)
+ }
+ nodeSet[nodeID] = struct{}{}
+ }
+ nodes := make([]string, 0, len(nodeSet))
+ for nodeID := range nodeSet {
+ nodes = append(nodes, nodeID)
+ }
+ sort.Strings(nodes)
+ return nodes, nil
+}
+
func (n *clusterNodeService) OnAddOrUpdate(metadata schema.Metadata) {
switch metadata.Kind {
case schema.KindNode:
@@ -117,3 +141,8 @@ func NewLocalNodeRegistry() NodeRegistry {
func (localNodeService) Locate(_, _ string, _, _ uint32) (string, error) {
return "local", nil
}
+
+// LocateAll of localNodeService always returns [local].
+func (localNodeService) LocateAll(_ string, _ uint32, _ int) ([]string, error) {
+ return []string{"local"}, nil
+}
diff --git a/banyand/liaison/grpc/node_test.go b/banyand/liaison/grpc/node_test.go
index 999d78d14..89c11946c 100644
--- a/banyand/liaison/grpc/node_test.go
+++ b/banyand/liaison/grpc/node_test.go
@@ -58,4 +58,15 @@ func TestClusterNodeRegistry(t *testing.T) {
nodeID, err := cnr.Locate("metrics", "instance_traffic", 0, 0)
assert.NoError(t, err)
assert.Equal(t, fakeNodeID, nodeID)
+
+ nodes, locateAllErr := cnr.LocateAll("metrics", 0, 3)
+ assert.NoError(t, locateAllErr)
+ assert.Equal(t, []string{fakeNodeID}, nodes)
+}
+
+func TestLocalNodeRegistry_LocateAll(t *testing.T) {
+ nr := NewLocalNodeRegistry()
+ nodes, err := nr.LocateAll("any-group", 0, 3)
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"local"}, nodes)
}
diff --git a/banyand/measure/metadata.go b/banyand/measure/metadata.go
index 0ddc6b101..d846f5f0d 100644
--- a/banyand/measure/metadata.go
+++ b/banyand/measure/metadata.go
@@ -801,18 +801,10 @@ func (s *queueSupplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB,
SubQueueCreator: newWriteQueue,
GetNodes: func(shardID common.ShardID) []string {
copies := ro.Replicas + 1
- nodeSet := make(map[string]struct{}, copies)
- for i := uint32(0); i < copies; i++ {
- nodeID, err := s.measureDataNodeRegistry.Locate(group, "", uint32(shardID), i)
- if err != nil {
- s.l.Error().Err(err).Str("group", group).Uint32("shard", uint32(shardID)).Uint32("copy", i).Msg("failed to locate node")
- return nil
- }
- nodeSet[nodeID] = struct{}{}
- }
- nodes := make([]string, 0, len(nodeSet))
- for nodeID := range nodeSet {
- nodes = append(nodes, nodeID)
+ nodes, err := s.measureDataNodeRegistry.LocateAll(group, uint32(shardID), int(copies))
+ if err != nil {
+ s.l.Error().Err(err).Str("group", group).Uint32("shard", uint32(shardID)).Msg("failed to locate nodes")
+ return nil
}
return nodes
},
diff --git a/banyand/stream/metadata.go b/banyand/stream/metadata.go
index 1f68bb264..96c810213 100644
--- a/banyand/stream/metadata.go
+++ b/banyand/stream/metadata.go
@@ -665,18 +665,10 @@ func (s *queueSupplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB,
SubQueueCreator: newWriteQueue,
GetNodes: func(shardID common.ShardID) []string {
copies := ro.Replicas + 1
- nodeSet := make(map[string]struct{}, copies)
- for i := uint32(0); i < copies; i++ {
- nodeID, err := s.streamDataNodeRegistry.Locate(group, "", uint32(shardID), i)
- if err != nil {
- s.l.Error().Err(err).Str("group", group).Uint32("shard", uint32(shardID)).Uint32("copy", i).Msg("failed to locate node")
- return nil
- }
- nodeSet[nodeID] = struct{}{}
- }
- nodes := make([]string, 0, len(nodeSet))
- for nodeID := range nodeSet {
- nodes = append(nodes, nodeID)
+ nodes, err := s.streamDataNodeRegistry.LocateAll(group, uint32(shardID), int(copies))
+ if err != nil {
+ s.l.Error().Err(err).Str("group", group).Uint32("shard", uint32(shardID)).Msg("failed to locate nodes")
+ return nil
}
return nodes
},
diff --git a/banyand/trace/handoff_controller.go b/banyand/trace/handoff_controller.go
index 4e69f7559..bc6f176f2 100644
--- a/banyand/trace/handoff_controller.go
+++ b/banyand/trace/handoff_controller.go
@@ -467,14 +467,12 @@ func (hc *handoffController) calculateOfflineNodes(onlineNodes []string, group s
continue
}
seen[node] = struct{}{}
- if !hc.isNodeHealthy(node) {
- offlineNodes = append(offlineNodes, node)
+ // Online nodes are delivered by the syncer; only enqueue truly offline nodes.
+ if _, isOnline := onlineSet[node]; isOnline {
continue
}
- if len(onlineSet) > 0 {
- if _, isOnline := onlineSet[node]; !isOnline {
- offlineNodes = append(offlineNodes, node)
- }
+ if !hc.isNodeHealthy(node) {
+ offlineNodes = append(offlineNodes, node)
continue
}
if hc.tire2Client == nil {
diff --git a/banyand/trace/handoff_storage_test.go b/banyand/trace/handoff_storage_test.go
index 4ca00ca8f..c9f35b1ed 100644
--- a/banyand/trace/handoff_storage_test.go
+++ b/banyand/trace/handoff_storage_test.go
@@ -627,6 +627,70 @@ func TestHandoffController_FiltersNonOwningOfflineNodes(t *testing.T) {
tester.True(os.IsNotExist(statErr), "expected no queue directory to be created for non-owning node")
}
+func TestHandoffController_OnlineNodeSkipsStaleHealthCheck(t *testing.T) {
+ tester := require.New(t)
+ tempDir, deferFunc := test.Space(tester)
+ defer deferFunc()
+
+ lfs := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+ dataNodes := []string{"node1:17912", "node2:17912"}
+ const groupName = "group1"
+ const shardID uint32 = 0
+
+ resolver := func(string, uint32) ([]string, error) {
+ return dataNodes, nil
+ }
+
+ // Health check reports only node1 (simulating stale health after node2 restart).
+ queueClient := &fakeQueueClient{healthy: []string{"node1:17912"}}
+ hc, err := newHandoffController(lfs, tempDir, queueClient, dataNodes, 0, l, resolver)
+ tester.NoError(err)
+ defer hc.close()
+
+ // Both nodes are in the syncer's target list, but health check is stale for node2.
+ offline := hc.calculateOfflineNodes(
+ []string{"node1:17912", "node2:17912"},
+ groupName,
+ common.ShardID(shardID),
+ )
+ tester.Len(offline, 0,
+ "expected no offline nodes when both are in the syncer target list, even with stale health check")
+}
+
+// TestHandoffController_OfflineNodeNotInOnlineSet verifies that a node NOT in the syncer's
+// target list and reported unhealthy is correctly classified as offline.
+func TestHandoffController_OfflineNodeNotInOnlineSet(t *testing.T) {
+ tester := require.New(t)
+ tempDir, deferFunc := test.Space(tester)
+ defer deferFunc()
+
+ lfs := fs.NewLocalFileSystem()
+ l := logger.GetLogger("test")
+ dataNodes := []string{"node1:17912", "node2:17912"}
+ const groupName = "group1"
+ const shardID uint32 = 0
+
+ resolver := func(string, uint32) ([]string, error) {
+ return dataNodes, nil
+ }
+
+ // Health check reports only node1 — node2 is offline.
+ queueClient := &fakeQueueClient{healthy: []string{"node1:17912"}}
+ hc, err := newHandoffController(lfs, tempDir, queueClient, dataNodes, 0, l, resolver)
+ tester.NoError(err)
+ defer hc.close()
+
+ // Only node1 is in the syncer's target list (simulating registry before AddNode).
+ offline := hc.calculateOfflineNodes(
+ []string{"node1:17912"},
+ groupName,
+ common.ShardID(shardID),
+ )
+ tester.Len(offline, 1)
+ tester.Equal("node2:17912", offline[0])
+}
+
// TestHandoffController_SizeRecovery verifies that total size is correctly calculated on restart.
func TestHandoffController_SizeRecovery(t *testing.T) {
tester := require.New(t)
diff --git a/banyand/trace/metadata.go b/banyand/trace/metadata.go
index 1fc9affa2..5bbdfe174 100644
--- a/banyand/trace/metadata.go
+++ b/banyand/trace/metadata.go
@@ -690,18 +690,10 @@ func (qs *queueSupplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB,
},
GetNodes: func(shardID common.ShardID) []string {
copies := ro.Replicas + 1
- nodeSet := make(map[string]struct{}, copies)
- for i := uint32(0); i < copies; i++ {
- nodeID, err := qs.traceDataNodeRegistry.Locate(group, "", uint32(shardID), i)
- if err != nil {
- qs.l.Error().Err(err).Str("group", group).Uint32("shard", uint32(shardID)).Uint32("copy", i).Msg("failed to locate node")
- return nil
- }
- nodeSet[nodeID] = struct{}{}
- }
- nodes := make([]string, 0, len(nodeSet))
- for nodeID := range nodeSet {
- nodes = append(nodes, nodeID)
+ nodes, err := qs.traceDataNodeRegistry.LocateAll(group, uint32(shardID), int(copies))
+ if err != nil {
+ qs.l.Error().Err(err).Str("group", group).Uint32("shard", uint32(shardID)).Msg("failed to locate nodes")
+ return nil
}
return nodes
},
diff --git a/banyand/trace/svc_liaison.go b/banyand/trace/svc_liaison.go
index 5532ce011..3613b6c48 100644
--- a/banyand/trace/svc_liaison.go
+++ b/banyand/trace/svc_liaison.go
@@ -23,7 +23,6 @@ import (
"fmt"
"path"
"path/filepath"
- "sort"
"time"
"github.com/dustin/go-humanize"
@@ -253,22 +252,7 @@ func (l *liaison) PreRun(ctx context.Context) error {
return nil, fmt.Errorf("group %s missing resource options", group)
}
copies := groupSchema.ResourceOpts.Replicas + 1
- if len(l.dataNodeList) == 0 {
- return nil, fmt.Errorf("no data nodes configured for handoff")
- }
- sortedNodes := append([]string(nil), l.dataNodeList...)
- sort.Strings(sortedNodes)
- nodes := make([]string, 0, copies)
- seen := make(map[string]struct{}, copies)
- for replica := uint32(0); replica < copies; replica++ {
- nodeID := sortedNodes[(int(shardID)+int(replica))%len(sortedNodes)]
- if _, ok := seen[nodeID]; ok {
- continue
- }
- nodes = append(nodes, nodeID)
- seen[nodeID] = struct{}{}
- }
- return nodes, nil
+ return traceDataNodeRegistry.LocateAll(group, shardID, int(copies))
}
// nolint:contextcheck
(skywalking-banyandb) 20/24: fix: Use `topic` instead of `session_id` as the Prometheus label (#1093)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 85d14e6d3c6c4bb622650354eaeb827480fe34c0
Author: OmCheeLin <19...@163.com>
AuthorDate: Wed Apr 29 11:50:27 2026 +0800
fix: Use `topic` instead of `session_id` as the Prometheus label (#1093)
* fix(queue/sub): label chunk-ordering metrics by topic not session_id
session_id is per-sync UUID and causes unbounded Prometheus cardinality.
Use sync metadata topic at each updateChunkOrderMetrics call site.
Co-authored-by: Gao Hongtao <ha...@gmail.com>
---
banyand/queue/sub/chunked_sync.go | 16 ++--
banyand/queue/sub/chunked_sync_test.go | 129 +++++++++++++++++++++++++++++++++
banyand/queue/sub/server.go | 26 +++----
3 files changed, 152 insertions(+), 19 deletions(-)
diff --git a/banyand/queue/sub/chunked_sync.go b/banyand/queue/sub/chunked_sync.go
index 07fa6eafe..7651a52e6 100644
--- a/banyand/queue/sub/chunked_sync.go
+++ b/banyand/queue/sub/chunked_sync.go
@@ -159,7 +159,7 @@ func (s *server) SyncPart(stream clusterv1.ChunkedSyncService_SyncPartServer) er
if currentSession.partCtx != nil {
if currentSession.partCtx.Handler != nil {
if finishErr := currentSession.partCtx.Handler.FinishSync(); finishErr != nil {
- s.updateChunkOrderMetrics("finish_sync_err", currentSession.sessionID)
+ s.updateChunkOrderMetrics("finish_sync_err", currentSession.metadata.Topic)
s.log.Error().Err(finishErr).Str("session_id", currentSession.sessionID).Msg("failed to finish sync for previous session")
}
if closeErr := currentSession.partCtx.Close(); closeErr != nil {
@@ -253,6 +253,10 @@ func (s *server) processChunkSequential(stream clusterv1.ChunkedSyncService_Sync
func (s *server) processChunkWithReordering(stream clusterv1.ChunkedSyncService_SyncPartServer, session *syncSession, req *clusterv1.SyncPartRequest) error {
buffer := session.chunkBuffer
+ // must check buffer timeout before refreshing lastActivity, otherwise it will never timeout.
+ if err := s.checkBufferTimeout(session); err != nil {
+ return err
+ }
buffer.lastActivity = time.Now()
if req.ChunkIndex == buffer.expectedIndex {
@@ -266,7 +270,7 @@ func (s *server) processChunkWithReordering(stream clusterv1.ChunkedSyncService_
if req.ChunkIndex > buffer.expectedIndex {
gap := req.ChunkIndex - buffer.expectedIndex
- s.updateChunkOrderMetrics("out_of_order_received", req.SessionId)
+ s.updateChunkOrderMetrics("out_of_order_received", session.metadata.Topic)
if gap > s.maxChunkGapSize {
errMsg := fmt.Sprintf("chunk gap too large: expected %d, got %d (gap: %d > max: %d)",
@@ -277,7 +281,7 @@ func (s *server) processChunkWithReordering(stream clusterv1.ChunkedSyncService_
Uint32("gap", gap).
Uint32("max_gap", s.maxChunkGapSize).
Msg("chunk gap too large, rejecting")
- s.updateChunkOrderMetrics("gap_too_large", req.SessionId)
+ s.updateChunkOrderMetrics("gap_too_large", session.metadata.Topic)
return s.sendResponse(stream, req, clusterv1.SyncStatus_SYNC_STATUS_CHUNK_OUT_OF_ORDER, errMsg, nil)
}
@@ -288,7 +292,7 @@ func (s *server) processChunkWithReordering(stream clusterv1.ChunkedSyncService_
Uint32("buffer_size", uint32(len(buffer.chunks))).
Uint32("max_buffer_size", buffer.maxBufferSize).
Msg("chunk buffer full, rejecting chunk")
- s.updateChunkOrderMetrics("buffer_full", req.SessionId)
+ s.updateChunkOrderMetrics("buffer_full", session.metadata.Topic)
return s.sendResponse(stream, req, clusterv1.SyncStatus_SYNC_STATUS_CHUNK_OUT_OF_ORDER, errMsg, nil)
}
@@ -301,7 +305,7 @@ func (s *server) processChunkWithReordering(stream clusterv1.ChunkedSyncService_
Uint32("buffered_chunks", uint32(len(buffer.chunks))).
Msg("buffered out-of-order chunk")
}
- s.updateChunkOrderMetrics("chunk_buffered", req.SessionId)
+ s.updateChunkOrderMetrics("chunk_buffered", session.metadata.Topic)
return s.sendResponse(stream, req, clusterv1.SyncStatus_SYNC_STATUS_CHUNK_RECEIVED,
fmt.Sprintf("chunk %d buffered (waiting for %d)", req.ChunkIndex, buffer.expectedIndex), nil)
@@ -444,7 +448,7 @@ func (s *server) checkBufferTimeout(session *syncSession) error {
missing = append(missing, i)
}
}
-
+ s.updateChunkOrderMetrics("buffer_timeout", session.metadata.Topic)
return fmt.Errorf("buffer timeout: missing chunks %v after %v",
missing, session.chunkBuffer.bufferTimeout)
}
diff --git a/banyand/queue/sub/chunked_sync_test.go b/banyand/queue/sub/chunked_sync_test.go
index 0a5922e5a..8a7430f31 100644
--- a/banyand/queue/sub/chunked_sync_test.go
+++ b/banyand/queue/sub/chunked_sync_test.go
@@ -21,6 +21,7 @@ import (
"context"
"fmt"
"hash/crc32"
+ "sync"
"testing"
"time"
@@ -120,6 +121,9 @@ func TestChunkedSyncOutOfOrderHandling(t *testing.T) {
enableChunkReordering: tt.enableChunkReordering,
maxChunkBufferSize: tt.maxChunkBufferSize,
maxChunkGapSize: tt.maxChunkGapSize,
+ // Avoid accidental immediate buffer-timeout when bufferTimeout is zero.
+ // Some tests buffer out-of-order chunks and expect no error.
+ chunkBufferTimeout: time.Hour,
}
// Register a mock handler
@@ -189,6 +193,127 @@ func TestChunkedSyncOutOfOrderHandling(t *testing.T) {
}
}
+type capturingCounter struct {
+ labelValues [][]string
+ mu sync.Mutex
+}
+
+func (c *capturingCounter) Inc(_ float64, labelValues ...string) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ cp := make([]string, len(labelValues))
+ copy(cp, labelValues)
+ c.labelValues = append(c.labelValues, cp)
+}
+
+func (c *capturingCounter) Delete(_ ...string) bool {
+ return true
+}
+
+func (c *capturingCounter) uniqueFirstLabelValues() map[string]struct{} {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+
+ m := make(map[string]struct{})
+ for _, lv := range c.labelValues {
+ if len(lv) > 0 {
+ m[lv[0]] = struct{}{}
+ }
+ }
+ return m
+}
+
+func TestChunkOrderingMetricsAreLabeledByTopic_NotSessionID(t *testing.T) {
+ // enable reordering, otherwise the chunk-ordering metrics will not be triggered.
+ s := &server{
+ log: logger.GetLogger("test-server-metrics-label"),
+ chunkedSyncHandlers: make(map[bus.Topic]queue.ChunkedSyncHandler),
+ enableChunkReordering: true,
+ maxChunkBufferSize: 10,
+ maxChunkGapSize: 5,
+ }
+
+ // handler: avoid "no handler registered" in processExpectedChunk.
+ mockHandler := &MockChunkedSyncHandler{}
+ s.chunkedSyncHandlers[data.TopicStreamPartSync] = mockHandler
+
+ // metrics: this test will trigger at least two events:
+ // - out_of_order_received
+ // - chunk_buffered
+ // so must put both counters, otherwise nil.Inc will panic.
+ outOfOrder := &capturingCounter{}
+ buffered := &capturingCounter{}
+ s.metrics = &metrics{
+ outOfOrderChunksReceived: outOfOrder,
+ chunksBuffered: buffered,
+ // other counters will not be triggered in this test, leave them nil
+ }
+
+ topic := data.TopicStreamPartSync.String()
+
+ drive := func(sessionID string) {
+ mockStream := &MockSyncPartStream{}
+ session := &syncSession{
+ sessionID: sessionID,
+ startTime: time.Now(),
+ chunksReceived: 0,
+ partsProgress: make(map[int]*partProgress),
+ metadata: &clusterv1.SyncMetadata{
+ Group: "test-group",
+ Topic: topic,
+ },
+ }
+
+ // send chunk 0 (establish buffer.expectedIndex=1)
+ req0 := &clusterv1.SyncPartRequest{
+ SessionId: sessionID,
+ ChunkIndex: 0,
+ ChunkData: []byte("chunk-0"),
+ ChunkChecksum: fmt.Sprintf("%x", crc32.ChecksumIEEE([]byte("chunk-0"))),
+ PartsInfo: []*clusterv1.PartInfo{
+ {Id: 1, Files: []*clusterv1.FileInfo{{Name: "f", Offset: 0, Size: 7}}},
+ },
+ }
+ require.NoError(t, s.processChunk(mockStream, session, req0))
+
+ // send chunk 2 (out-of-order: expected 1 got 2),
+ // will trigger out_of_order_received + chunk_buffered.
+ req2 := &clusterv1.SyncPartRequest{
+ SessionId: sessionID,
+ ChunkIndex: 2,
+ ChunkData: []byte("chunk-2"),
+ ChunkChecksum: fmt.Sprintf("%x", crc32.ChecksumIEEE([]byte("chunk-2"))),
+ PartsInfo: []*clusterv1.PartInfo{
+ {Id: 2, Files: []*clusterv1.FileInfo{{Name: "f", Offset: 0, Size: 7}}},
+ },
+ }
+ require.NoError(t, s.processChunk(mockStream, session, req2))
+ }
+
+ drive("test-session-A")
+ drive("test-session-B")
+
+ // assert: labelValues[0] must be topic; and unique label must be only one (topic)
+ uniqOut := outOfOrder.uniqueFirstLabelValues()
+ uniqBuf := buffered.uniqueFirstLabelValues()
+
+ assert.Equal(t, 1, len(uniqOut))
+ assert.Equal(t, 1, len(uniqBuf))
+
+ _, okOut := uniqOut[topic]
+ _, okBuf := uniqBuf[topic]
+ assert.True(t, okOut, "out_of_order_received label must be topic")
+ assert.True(t, okBuf, "chunk_buffered label must be topic")
+
+ // assert: never should have session_id as label
+ _, bad1 := uniqOut["test-session-A"]
+ _, bad2 := uniqOut["test-session-B"]
+ _, bad3 := uniqBuf["test-session-A"]
+ _, bad4 := uniqBuf["test-session-B"]
+ assert.False(t, bad1 || bad2 || bad3 || bad4, "metrics must not be labeled by session_id")
+}
+
// MockChunkedSyncHandler implements queue.ChunkedSyncHandler for testing.
type MockChunkedSyncHandler struct{}
@@ -264,6 +389,10 @@ func TestChunkedSyncBufferTimeout(t *testing.T) {
session := &syncSession{
sessionID: "test-session-timeout",
startTime: time.Now(),
+ metadata: &clusterv1.SyncMetadata{
+ Topic: data.TopicStreamPartSync.String(),
+ Group: "test-group",
+ },
chunkBuffer: &chunkBuffer{
chunks: make(map[uint32]*clusterv1.SyncPartRequest),
expectedIndex: 1, // Waiting for chunk 1
diff --git a/banyand/queue/sub/server.go b/banyand/queue/sub/server.go
index ffe482229..db9188ee7 100644
--- a/banyand/queue/sub/server.go
+++ b/banyand/queue/sub/server.go
@@ -435,32 +435,32 @@ func newMetrics(factory observability.Factory) *metrics {
totalMsgSentErr: factory.NewCounter("total_msg_sent_err", "topic"),
// Chunk ordering metrics
- outOfOrderChunksReceived: factory.NewCounter("out_of_order_chunks_received", "session_id"),
- chunksBuffered: factory.NewCounter("chunks_buffered", "session_id"),
- bufferTimeouts: factory.NewCounter("buffer_timeouts", "session_id"),
- largeGapsRejected: factory.NewCounter("large_gaps_rejected", "session_id"),
- bufferCapacityExceeded: factory.NewCounter("buffer_capacity_exceeded", "session_id"),
- finishSyncErr: factory.NewCounter("finish_sync_err", "session_id"),
+ outOfOrderChunksReceived: factory.NewCounter("out_of_order_chunks_received", "topic"),
+ chunksBuffered: factory.NewCounter("chunks_buffered", "topic"),
+ bufferTimeouts: factory.NewCounter("buffer_timeouts", "topic"),
+ largeGapsRejected: factory.NewCounter("large_gaps_rejected", "topic"),
+ bufferCapacityExceeded: factory.NewCounter("buffer_capacity_exceeded", "topic"),
+ finishSyncErr: factory.NewCounter("finish_sync_err", "topic"),
}
}
// updateChunkOrderMetrics updates chunk ordering metrics.
-func (s *server) updateChunkOrderMetrics(event, sessionID string) {
+func (s *server) updateChunkOrderMetrics(event, topic string) {
if s.metrics == nil {
return // Skip metrics if not initialized (e.g., during tests)
}
switch event {
case "out_of_order_received":
- s.metrics.outOfOrderChunksReceived.Inc(1, sessionID)
+ s.metrics.outOfOrderChunksReceived.Inc(1, topic)
case "chunk_buffered":
- s.metrics.chunksBuffered.Inc(1, sessionID)
+ s.metrics.chunksBuffered.Inc(1, topic)
case "buffer_timeout":
- s.metrics.bufferTimeouts.Inc(1, sessionID)
+ s.metrics.bufferTimeouts.Inc(1, topic)
case "gap_too_large":
- s.metrics.largeGapsRejected.Inc(1, sessionID)
+ s.metrics.largeGapsRejected.Inc(1, topic)
case "buffer_full":
- s.metrics.bufferCapacityExceeded.Inc(1, sessionID)
+ s.metrics.bufferCapacityExceeded.Inc(1, topic)
case "finish_sync_err":
- s.metrics.finishSyncErr.Inc(1, sessionID)
+ s.metrics.finishSyncErr.Inc(1, topic)
}
}
(skywalking-banyandb) 04/24: fix(storage): disable rotation task on warm and cold lifecycle nodes (#1055)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit d01e715ef3646e9386ace26a043dfd629304aa2d
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Thu Apr 9 19:47:00 2026 +0800
fix(storage): disable rotation task on warm and cold lifecycle nodes (#1055)
* fix(storage): disable rotation task on warm and cold lifecycle nodes
The rotation task uses NextTime(eventTime) which produces incorrect
segment boundaries for multi-day intervals (e.g., 3-day warm stage
interval). Warm and cold nodes receive data via lifecycle migration
which creates segments on demand via CreateSegmentIfNotExist with
correct boundaries. Disabling rotation on non-hot nodes prevents
the issue.
---
banyand/internal/storage/rotation.go | 2 +-
banyand/internal/storage/rotation_test.go | 69 +++++++++++++++++++++++++++++++
banyand/internal/storage/tsdb.go | 3 ++
banyand/measure/metadata.go | 4 ++
banyand/stream/metadata.go | 4 ++
banyand/trace/metadata.go | 4 ++
docs/concept/rotation.md | 9 ++++
docs/operation/lifecycle.md | 9 ++++
8 files changed, 103 insertions(+), 1 deletion(-)
diff --git a/banyand/internal/storage/rotation.go b/banyand/internal/storage/rotation.go
index 7d1a737aa..fd30f3edd 100644
--- a/banyand/internal/storage/rotation.go
+++ b/banyand/internal/storage/rotation.go
@@ -100,7 +100,7 @@ func (d *database[T, O]) startRotationTask() error {
gap := latest.End.UnixNano() - ts
// gap <=0 means the event is from the future
// the segment will be created by a written event directly
- if gap <= 0 || gap > newSegmentTimeGap {
+ if gap <= 0 || gap > newSegmentTimeGap || d.disableRotation {
return
}
d.incTotalRotationStarted(1)
diff --git a/banyand/internal/storage/rotation_test.go b/banyand/internal/storage/rotation_test.go
index e9e003916..b9251a357 100644
--- a/banyand/internal/storage/rotation_test.go
+++ b/banyand/internal/storage/rotation_test.go
@@ -238,3 +238,72 @@ func (m *MockMetrics) Factory() observability.Factory {
}
var MockMetricsCreator = func(_ common.Position) Metrics { return &MockMetrics{} }
+
+func TestRotationDisabled(t *testing.T) {
+ t.Run("rotation should not create segments when disabled", func(t *testing.T) {
+ dir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ TSDBOpts := TSDBOpts[*MockTSTable, any]{
+ Location: dir,
+ SegmentInterval: IntervalRule{Unit: DAY, Num: 3},
+ TTL: IntervalRule{Unit: DAY, Num: 30},
+ ShardNum: 1,
+ TSTableCreator: MockTSTableCreator,
+ DisableRotation: true,
+ }
+ ctx := context.Background()
+ mc := timestamp.NewMockClock()
+ ts, err := time.ParseInLocation("2006-01-02 15:04:05", "2024-05-01 00:00:00", time.Local)
+ require.NoError(t, err)
+ mc.Set(ts)
+ ctx = timestamp.SetClock(ctx, mc)
+
+ sc := NewServiceCache()
+ tsdb, err := OpenTSDB(ctx, TSDBOpts, sc, group)
+ require.NoError(t, err)
+ defer tsdb.Close()
+
+ // Create initial segment for day 1
+ seg, err := tsdb.CreateSegmentIfNotExist(ts)
+ require.NoError(t, err)
+ seg.DecRef()
+
+ db := tsdb.(*database[*MockTSTable, any])
+ segCtrl := db.segmentController
+
+ // Simulate data arriving day by day for 6 days (mimicking lifecycle migration)
+ // Day 1 (05-01): already in segment [05-01, 05-04)
+ // Day 2 (05-02): still in [05-01, 05-04)
+ // Day 3 (05-03): still in [05-01, 05-04)
+ // Day 4 (05-04): need new segment [05-04, 05-07)
+ for day := 0; day < 6; day++ {
+ dayTime := ts.AddDate(0, 0, day)
+ mc.Set(dayTime)
+ // Simulate what migration does: CreateSegmentIfNotExist then Tick
+ seg, segErr := tsdb.CreateSegmentIfNotExist(dayTime)
+ require.NoError(t, segErr)
+ seg.DecRef()
+ // Tick with max timestamp near end of day (like migration does with ctx.MaxTimestamp)
+ maxTS := dayTime.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
+ tsdb.Tick(maxTS.UnixNano())
+ }
+
+ // Verify: only 2 segments created, both with correct 3-day boundaries
+ segments, _ := segCtrl.segments(false)
+ defer func() {
+ for i := range segments {
+ segments[i].DecRef()
+ }
+ }()
+ require.Equal(t, 2, len(segments), "expected 2 segments for 6 days with 3-day interval")
+
+ // First segment: [05-01, 05-04)
+ assert.Equal(t, "2024-05-01 00:00:00", segments[0].Start.Format("2006-01-02 15:04:05"))
+ assert.Equal(t, "2024-05-04 00:00:00", segments[0].End.Format("2006-01-02 15:04:05"))
+
+ // Second segment: [05-04, 05-07)
+ assert.Equal(t, "2024-05-04 00:00:00", segments[1].Start.Format("2006-01-02 15:04:05"))
+ assert.Equal(t, "2024-05-07 00:00:00", segments[1].End.Format("2006-01-02 15:04:05"))
+ })
+}
diff --git a/banyand/internal/storage/tsdb.go b/banyand/internal/storage/tsdb.go
index 16a9c387f..fa20507e4 100644
--- a/banyand/internal/storage/tsdb.go
+++ b/banyand/internal/storage/tsdb.go
@@ -63,6 +63,7 @@ type TSDBOpts[T TSTable, O any] struct {
SeriesIndexCacheMaxBytes int
ShardNum uint32
DisableRetention bool
+ DisableRotation bool
SegmentIdleTimeout time.Duration
MemoryLimit uint64
}
@@ -156,6 +157,7 @@ type database[T TSTable, O any] struct {
rotationProcessOn atomic.Bool
closed atomic.Bool
disableRetention bool
+ disableRotation bool
}
func (d *database[T, O]) Close() error {
@@ -238,6 +240,7 @@ func OpenTSDB[T TSTable, O any](ctx context.Context, opts TSDBOpts[T, O], cache
return nil, err
}
obsservice.MetricsCollector.Register(location, db.collect)
+ db.disableRotation = opts.DisableRotation
return db, db.startRotationTask()
}
diff --git a/banyand/measure/metadata.go b/banyand/measure/metadata.go
index 1517b8e68..0ddc6b101 100644
--- a/banyand/measure/metadata.go
+++ b/banyand/measure/metadata.go
@@ -681,6 +681,7 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
segInterval := ro.SegmentInterval
segmentIdleTimeout := time.Duration(0)
disableRetention := false
+ disableRotation := false
if len(ro.Stages) > 0 && len(s.nodeLabels) > 0 {
var ttlNum uint32
foundMatched := false
@@ -704,10 +705,12 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
segmentIdleTimeout = 5 * time.Minute
}
disableRetention = i+1 < len(ro.Stages)
+ disableRotation = true
break
}
if !foundMatched {
disableRetention = true
+ disableRotation = true
}
}
group := groupSchema.Metadata.Name
@@ -724,6 +727,7 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
StorageMetricsFactory: factory,
SegmentIdleTimeout: segmentIdleTimeout,
DisableRetention: disableRetention,
+ DisableRotation: disableRotation,
MemoryLimit: s.pm.GetLimit(),
}
return storage.OpenTSDB(
diff --git a/banyand/stream/metadata.go b/banyand/stream/metadata.go
index 4bf425cda..1f68bb264 100644
--- a/banyand/stream/metadata.go
+++ b/banyand/stream/metadata.go
@@ -537,6 +537,7 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
segInterval := ro.SegmentInterval
segmentIdleTimeout := time.Duration(0)
disableRetention := false
+ disableRotation := false
if len(ro.Stages) > 0 && len(s.nodeLabels) > 0 {
var ttlNum uint32
foundMatched := false
@@ -560,10 +561,12 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
segmentIdleTimeout = 5 * time.Minute
}
disableRetention = i+1 < len(ro.Stages)
+ disableRotation = true
break
}
if !foundMatched {
disableRetention = true
+ disableRotation = true
}
}
group := groupSchema.Metadata.Name
@@ -580,6 +583,7 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
StorageMetricsFactory: s.omr.With(storageScope.ConstLabels(meter.ToLabelPairs(common.DBLabelNames(), p.DBLabelValues()))),
SegmentIdleTimeout: segmentIdleTimeout,
DisableRetention: disableRetention,
+ DisableRotation: disableRotation,
MemoryLimit: s.pm.GetLimit(),
}
return storage.OpenTSDB(
diff --git a/banyand/trace/metadata.go b/banyand/trace/metadata.go
index 7377c4201..1fc9affa2 100644
--- a/banyand/trace/metadata.go
+++ b/banyand/trace/metadata.go
@@ -560,6 +560,7 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
segInterval := ro.SegmentInterval
segmentIdleTimeout := time.Duration(0)
disableRetention := false
+ disableRotation := false
if len(ro.Stages) > 0 && len(s.nodeLabels) > 0 {
var ttlNum uint32
foundMatched := false
@@ -583,10 +584,12 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
segmentIdleTimeout = 5 * time.Minute
}
disableRetention = i+1 < len(ro.Stages)
+ disableRotation = true
break
}
if !foundMatched {
disableRetention = true
+ disableRotation = true
}
}
group := groupSchema.Metadata.Name
@@ -603,6 +606,7 @@ func (s *supplier) OpenDB(groupSchema *commonv1.Group) (resourceSchema.DB, error
StorageMetricsFactory: s.omr.With(traceScope.ConstLabels(meter.ToLabelPairs(common.DBLabelNames(), p.DBLabelValues()))),
SegmentIdleTimeout: segmentIdleTimeout,
DisableRetention: disableRetention,
+ DisableRotation: disableRotation,
MemoryLimit: s.pm.GetLimit(),
}
return storage.OpenTSDB(
diff --git a/docs/concept/rotation.md b/docs/concept/rotation.md
index 9d4e4e0bf..68dfe1c39 100644
--- a/docs/concept/rotation.md
+++ b/docs/concept/rotation.md
@@ -137,6 +137,15 @@ S = 2
So, **2 segments** are required to retain data for 7 days with an 8-day segment interval. 2 segments are the minimum number whatever the TTL and segment interval are. When the TTL is less than the segment interval, you can have the minimum number of segments.
+## Rotation in Lifecycle Deployments
+
+In a hot-warm-cold lifecycle deployment, data rotation behaves differently depending on the node's role:
+
+- **Hot nodes**: Rotation is enabled. The rotation task pre-creates segments based on the hot stage's `segment_interval`.
+- **Warm and cold nodes**: Rotation is disabled. Segments are created on demand by the lifecycle migration process via `CreateSegmentIfNotExist`, using the target stage's `segment_interval`. Since the rotation task uses `NextTime(eventTime)` which produces incorrect boundaries for multi-day intervals, disabling it on warm/cold nodes ensures segments are always created with correct boundaries.
+
+Additionally, data retention (TTL-based segment deletion) is disabled on non-last-stage nodes, since the lifecycle agent manages data migration between stages instead.
+
## Conclusion
Data rotation is a critical aspect of managing data in BanyanDB. By understanding the relationship between the number of segments, segment interval, and TTL, you can effectively manage data retention and query performance in the database. The formula provided here offers a simple way to calculate the number of segments required based on the chosen segment interval and TTL.
diff --git a/docs/operation/lifecycle.md b/docs/operation/lifecycle.md
index 1c94496aa..791a688f6 100644
--- a/docs/operation/lifecycle.md
+++ b/docs/operation/lifecycle.md
@@ -130,6 +130,15 @@ lifecycle \
| `--etcd-endpoints` | Endpoints for etcd connections | `""` |
| `--schedule` | Schedule for periodic backup (e.g., @yearly, @monthly, @weekly, @daily, etc.) | `""` |
+## Automatic Behavior on Warm and Cold Nodes
+
+When a data node matches a lifecycle stage (i.e., its labels match a stage's `node_selector`), two behaviors are automatically configured:
+
+- **Rotation is disabled**: The rotation task (which pre-creates segments on hot nodes) is not started on warm or cold nodes. Segments are created on demand by the lifecycle migration process, ensuring correct segment boundaries for multi-day intervals.
+- **Retention is disabled on non-last stages**: TTL-based segment deletion is disabled on all stages except the last one. Data removal on intermediate stages is managed by the lifecycle migration process, not by the built-in retention mechanism.
+
+If a node's labels do not match any stage, both rotation and retention are disabled as a safety measure.
+
## Best Practices
1. **Node Labeling:**
(skywalking-banyandb) 07/24: fix(sidx): use MinTimestamp/MaxTimestamp instead of SegmentID in streaming sync (#1060)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 9634d8268e21e42addd644de7251a645fb27a8c6
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Sat Apr 11 07:57:50 2026 +0800
fix(sidx): use MinTimestamp/MaxTimestamp instead of SegmentID in streaming sync (#1060)
* fix(sidx): use MinTimestamp/MaxTimestamp instead of SegmentID in streaming sync
SIDX StreamingParts was incorrectly setting MinTimestamp from
partMetadata.SegmentID and omitting MaxTimestamp entirely. This caused
the receiving node to reject parts with "invalid MinTimestamp 0" when
SegmentID was zero, and previously created corrupt seg-19700101
directories before validation was added in #1059.
---
banyand/internal/sidx/sync.go | 19 +++-
banyand/internal/sidx/sync_test.go | 191 +++++++++++++++++++++++++++++++++++++
2 files changed, 207 insertions(+), 3 deletions(-)
diff --git a/banyand/internal/sidx/sync.go b/banyand/internal/sidx/sync.go
index 465df54eb..96352364f 100644
--- a/banyand/internal/sidx/sync.go
+++ b/banyand/internal/sidx/sync.go
@@ -74,7 +74,7 @@ func (s *sidx) StreamingParts(partIDsToSync map[uint64]struct{}, group string, s
part := pw.p
files, release := createPartFileReaders(part)
releaseFuncs = append(releaseFuncs, release)
- streamingParts = append(streamingParts, queue.StreamingPartData{
+ spd := queue.StreamingPartData{
ID: part.partMetadata.ID,
Group: group,
ShardID: shardID,
@@ -84,11 +84,24 @@ func (s *sidx) StreamingParts(partIDsToSync map[uint64]struct{}, group string, s
UncompressedSizeBytes: part.partMetadata.UncompressedSizeBytes,
TotalCount: part.partMetadata.TotalCount,
BlocksCount: part.partMetadata.BlocksCount,
- MinTimestamp: part.partMetadata.SegmentID,
MinKey: part.partMetadata.MinKey,
MaxKey: part.partMetadata.MaxKey,
PartType: name,
- })
+ }
+ switch {
+ case part.partMetadata.MinTimestamp != nil:
+ spd.MinTimestamp = *part.partMetadata.MinTimestamp
+ case part.partMetadata.SegmentID > 0:
+ spd.MinTimestamp = part.partMetadata.SegmentID
+ default:
+ logger.Panicf("sidx streaming parts: %s, part %d has no valid timestamp (MinTimestamp=nil, SegmentID=0)", name, part.partMetadata.ID)
+ }
+ if part.partMetadata.MaxTimestamp != nil {
+ spd.MaxTimestamp = *part.partMetadata.MaxTimestamp
+ } else {
+ spd.MaxTimestamp = spd.MinTimestamp
+ }
+ streamingParts = append(streamingParts, spd)
}
}
diff --git a/banyand/internal/sidx/sync_test.go b/banyand/internal/sidx/sync_test.go
new file mode 100644
index 000000000..ff6a32435
--- /dev/null
+++ b/banyand/internal/sidx/sync_test.go
@@ -0,0 +1,191 @@
+// Licensed to Apache Software Foundation (ASF) under one or more contributor
+// license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright
+// ownership. Apache Software Foundation (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 sidx
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-banyandb/api/data"
+ "github.com/apache/skywalking-banyandb/banyand/observability"
+ "github.com/apache/skywalking-banyandb/banyand/protector"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
+)
+
+// TestStreamingParts_Timestamps verifies that StreamingParts propagates
+// MinTimestamp and MaxTimestamp from partMetadata (not SegmentID) to
+// StreamingPartData. This prevents zero-timestamp segments from being
+// created on the receiving node during distributed sync.
+func TestStreamingParts_Timestamps(t *testing.T) {
+ reqs := []WriteRequest{
+ createTestWriteRequest(1, 100, "data1"),
+ createTestWriteRequest(1, 200, "data2"),
+ }
+
+ t.Run("with_timestamps_set", func(t *testing.T) {
+ sidxIface := createTestSIDX(t)
+ raw := sidxIface.(*sidx)
+ defer func() {
+ assert.NoError(t, raw.Close())
+ }()
+
+ minTS := int64(1700000000)
+ maxTS := int64(1700001000)
+ writeTestDataWithTimeRange(t, raw, reqs, 1, 1, &minTS, &maxTS)
+
+ flushIntro, err := raw.Flush(map[uint64]struct{}{1: {}})
+ require.NoError(t, err)
+ raw.IntroduceFlushed(flushIntro)
+ flushIntro.Release()
+
+ partIDs := map[uint64]struct{}{1: {}}
+ parts, releaseFuncs := raw.StreamingParts(partIDs, "test-group", 0, "test-sidx")
+ defer func() {
+ for _, release := range releaseFuncs {
+ release()
+ }
+ }()
+
+ require.Len(t, parts, 1)
+ assert.Equal(t, uint64(1), parts[0].ID)
+ assert.Equal(t, int64(1700000000), parts[0].MinTimestamp,
+ "MinTimestamp should come from partMetadata.MinTimestamp, not SegmentID")
+ assert.Equal(t, int64(1700001000), parts[0].MaxTimestamp,
+ "MaxTimestamp should come from partMetadata.MaxTimestamp")
+ assert.Equal(t, "test-group", parts[0].Group)
+ assert.Equal(t, uint32(0), parts[0].ShardID)
+ assert.Equal(t, data.TopicTracePartSync.String(), parts[0].Topic)
+ assert.Equal(t, "test-sidx", parts[0].PartType)
+ })
+
+ t.Run("nil_timestamps_fallback_to_segment_id", func(t *testing.T) {
+ sidxIface := createTestSIDX(t)
+ raw := sidxIface.(*sidx)
+ defer func() {
+ assert.NoError(t, raw.Close())
+ }()
+
+ // segmentID=1000, nil timestamps — should fall back to SegmentID
+ writeTestDataWithTimeRange(t, raw, reqs, 1000, 1, nil, nil)
+
+ flushIntro, err := raw.Flush(map[uint64]struct{}{1: {}})
+ require.NoError(t, err)
+ raw.IntroduceFlushed(flushIntro)
+ flushIntro.Release()
+
+ partIDs := map[uint64]struct{}{1: {}}
+ parts, releaseFuncs := raw.StreamingParts(partIDs, "test-group", 0, "test-sidx")
+ defer func() {
+ for _, release := range releaseFuncs {
+ release()
+ }
+ }()
+
+ require.Len(t, parts, 1)
+ assert.Equal(t, int64(1000), parts[0].MinTimestamp,
+ "MinTimestamp should fall back to SegmentID when partMetadata.MinTimestamp is nil")
+ assert.Equal(t, int64(1000), parts[0].MaxTimestamp,
+ "MaxTimestamp should fall back to MinTimestamp when partMetadata.MaxTimestamp is nil")
+ })
+
+ t.Run("nil_timestamps_zero_segment_id_panics", func(t *testing.T) {
+ sidxIface := createTestSIDX(t)
+ raw := sidxIface.(*sidx)
+ defer func() {
+ assert.NoError(t, raw.Close())
+ }()
+
+ // segmentID=0, nil timestamps — should panic
+ writeTestDataWithTimeRange(t, raw, reqs, 0, 1, nil, nil)
+
+ flushIntro, err := raw.Flush(map[uint64]struct{}{1: {}})
+ require.NoError(t, err)
+ raw.IntroduceFlushed(flushIntro)
+ flushIntro.Release()
+
+ partIDs := map[uint64]struct{}{1: {}}
+ assert.Panics(t, func() {
+ parts, releaseFuncs := raw.StreamingParts(partIDs, "test-group", 0, "test-sidx")
+ for _, release := range releaseFuncs {
+ release()
+ }
+ _ = parts
+ }, "Should panic when both MinTimestamp and SegmentID are zero")
+ })
+
+ t.Run("nil_snapshot_returns_nil", func(t *testing.T) {
+ sidxIface := createTestSIDX(t)
+ raw := sidxIface.(*sidx)
+ defer func() {
+ assert.NoError(t, raw.Close())
+ }()
+
+ partIDs := map[uint64]struct{}{1: {}}
+ parts, releaseFuncs := raw.StreamingParts(partIDs, "test-group", 0, "test-sidx")
+ defer func() {
+ for _, release := range releaseFuncs {
+ release()
+ }
+ }()
+ assert.Nil(t, parts)
+ })
+
+ t.Run("multiple_parts_sorted_by_id", func(t *testing.T) {
+ dir := t.TempDir()
+ fileSystem := fs.NewLocalFileSystem()
+ opts := NewDefaultOptions()
+ opts.Memory = protector.NewMemory(observability.NewBypassRegistry())
+ opts.Path = dir
+
+ sidxIface, err := NewSIDX(fileSystem, opts)
+ require.NoError(t, err)
+ raw := sidxIface.(*sidx)
+ defer func() {
+ assert.NoError(t, raw.Close())
+ }()
+
+ min1, max1 := int64(1700000000), int64(1700001000)
+ min2, max2 := int64(1800000000), int64(1800001000)
+ writeTestDataWithTimeRange(t, raw, reqs, 1, 2, &min1, &max1)
+ writeTestDataWithTimeRange(t, raw, reqs, 2, 3, &min2, &max2)
+
+ flushIntro, flushErr := raw.Flush(map[uint64]struct{}{2: {}, 3: {}})
+ require.NoError(t, flushErr)
+ raw.IntroduceFlushed(flushIntro)
+ flushIntro.Release()
+
+ partIDs := map[uint64]struct{}{2: {}, 3: {}}
+ parts, releaseFuncs := raw.StreamingParts(partIDs, "test-group", 0, "test-sidx")
+ defer func() {
+ for _, release := range releaseFuncs {
+ release()
+ }
+ }()
+
+ require.Len(t, parts, 2)
+ // Parts should be sorted by ID
+ assert.Equal(t, uint64(2), parts[0].ID)
+ assert.Equal(t, int64(1700000000), parts[0].MinTimestamp)
+ assert.Equal(t, int64(1700001000), parts[0].MaxTimestamp)
+ assert.Equal(t, uint64(3), parts[1].ID)
+ assert.Equal(t, int64(1800000000), parts[1].MinTimestamp)
+ assert.Equal(t, int64(1800001000), parts[1].MaxTimestamp)
+ })
+}
(skywalking-banyandb) 24/24: chore: update CHANGES.md and sync .gitignore from main
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 55c6007c2fde1637a53deac1a6dfd27e987ba4db
Author: Hongtao Gao <ha...@gmail.com>
AuthorDate: Wed Apr 29 13:06:45 2026 +0000
chore: update CHANGES.md and sync .gitignore from main
Remove 0.10.1 section from CHANGES.md and sync .gitignore from main.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <no...@hapi.run>
Co-Authored-By: Claude Opus 4.7 <no...@anthropic.com>
---
.gitignore | 8 ++++----
CHANGES.md | 17 +----------------
2 files changed, 5 insertions(+), 20 deletions(-)
diff --git a/.gitignore b/.gitignore
index e433fb61c..9af8ace51 100644
--- a/.gitignore
+++ b/.gitignore
@@ -75,12 +75,12 @@ gomock_reflect*
.cursor/
# Claude
-Claude.md
.claude/
-CLAUDE.md
+.omc/
# eBPF generated files and binaries
fodc/agent/internal/ktm/iomonitor/ebpf/generated/vmlinux.h
-# Codex
-AGENTS.md
+# OpenSSL serial files
+*.srl
+
diff --git a/CHANGES.md b/CHANGES.md
index 96ad037ec..80c4f6c93 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -33,19 +33,4 @@ Release Notes.
### Chores
- Upgrade Go and npm dependencies for CVE fixes.
-- Bump ui and mcp npm dependencies for CVE fixes.
-
-## 0.10.1
-
-### Bug Fixes
-
-- Fix reuse of byte arrays in min/max implementation causing data corruption.
-- Fix flaky trace query filtering due to non-deterministic sidx tag ordering.
-- Fix index-mode measure queries returning documents outside requested time range.
-- Fix nil pointer panic in segment collectMetrics during shutdown.
-- Fix entity tag handling in trace filter preventing TagIdx index mismatch.
-- Fix unstable tag filter matching order in sidx.
-- Fix property schema client connection instability after data node restart.
-- Fix duplicate TopN query execution in distributed measure queries.
-- Fix bydbctl validation for malformed YAML input.
-- Fix FODC agent test instability in Basic Metrics Buffering test.
\ No newline at end of file
+- Bump ui and mcp npm dependencies for CVE fixes.
\ No newline at end of file
(skywalking-banyandb) 23/24: chore(version): Cherry-pick bug fixes from main for v0.10.2
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 15259e5f4a1d40cd9da43ea9f2da2a41bc23b407
Author: Hongtao Gao <ha...@gmail.com>
AuthorDate: Wed Apr 29 12:51:26 2026 +0000
chore(version): Cherry-pick bug fixes from main for v0.10.2
Update CHANGES.md for 0.10.2 release and fix cherry-pick conflicts
in segment.go and merger_test.go to align with v0.10.x codebase.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <no...@hapi.run>
Co-Authored-By: Claude Opus 4.7 <no...@anthropic.com>
---
CHANGES.md | 33 +++
banyand/internal/storage/segment.go | 72 +++----
banyand/trace/merger_test.go | 417 ------------------------------------
3 files changed, 59 insertions(+), 463 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 56a73bfb3..96ad037ec 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -2,6 +2,39 @@
Release Notes.
+## 0.10.2
+
+### Bug Fixes
+
+- Fix reuse of byte arrays in min/max implementation causing data corruption.
+- Fix index-mode measure queries returning documents outside requested time range.
+- Fix nil pointer panic in segment collectMetrics during shutdown.
+- Fix property schema client connection instability after data node restart.
+- Fix take snapshot error when no data in the segment.
+- Fix(storage): disable rotation task on warm and cold lifecycle nodes.
+- Fix(storage): prevent epoch segment creation from zero timestamps.
+- Fix(sidx): use MinTimestamp/MaxTimestamp instead of SegmentID in streaming sync.
+- Fix(handoff): prevent size limit bypass and sidx timestamp corruption in handoff replay.
+- Fix(handoff): prevent enqueuing parts for online nodes via shared LocateAll.
+- Fix wrong backup path of schema property.
+- Fix OOM issue during migration when a group contains a large amount of data.
+- Fix lifecycle migration failure when the target stage has `close: true`.
+- Fix stale sync request blocking watch session channel.
+- Fix nil pointer panic in disk monitor during early initialization.
+- Fix FileSystemError not matching io/fs.ErrNotExist sentinel.
+- Fix(topn): deduplicate entities in TopN aggregation query.
+- Fix(stream): skip element-index visit when idx/ is absent.
+- Fix(metadata): widen FODC inspection broadcast deadline and parallelize InspectAll.
+- Fix(measure,stream,trace): eliminate flusher/introducer data race on shutdown.
+- Fix: use `topic` instead of `session_id` as the Prometheus label.
+- Fix(fodc): heal reconnect deadlock and add error message when no agents for lifecycle request.
+- Fix(MCP): add explicit validation for properties and tools, and harden the server.
+
+### Chores
+
+- Upgrade Go and npm dependencies for CVE fixes.
+- Bump ui and mcp npm dependencies for CVE fixes.
+
## 0.10.1
### Bug Fixes
diff --git a/banyand/internal/storage/segment.go b/banyand/internal/storage/segment.go
index 04a333507..e4239e0bc 100644
--- a/banyand/internal/storage/segment.go
+++ b/banyand/internal/storage/segment.go
@@ -19,7 +19,6 @@ package storage
import (
"context"
- "encoding/json"
"fmt"
"io/fs"
"path"
@@ -34,19 +33,18 @@ import (
"github.com/apache/skywalking-banyandb/api/common"
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+ "github.com/apache/skywalking-banyandb/pkg/convert"
banyanfs "github.com/apache/skywalking-banyandb/pkg/fs"
"github.com/apache/skywalking-banyandb/pkg/index/inverted"
"github.com/apache/skywalking-banyandb/pkg/logger"
"github.com/apache/skywalking-banyandb/pkg/timestamp"
)
-var (
- // ErrSegmentClosed is returned when trying to access a closed segment.
- ErrSegmentClosed = errors.New("segment closed")
- // ErrInvalidSegmentTimestamp is returned when a segment timestamp is zero or near-epoch,
- // indicating a corrupted timestamp (e.g., unset MinTimestamp flowing through sync paths).
- ErrInvalidSegmentTimestamp = errors.New("invalid segment timestamp: epoch or near-epoch time is not valid for APM data")
-)
+// ErrSegmentClosed is returned when trying to access a closed segment.
+var ErrSegmentClosed = errors.New("segment closed")
+
+// ErrInvalidSegmentTimestamp is returned when a segment has an invalid timestamp (e.g., epoch/zero).
+var ErrInvalidSegmentTimestamp = errors.New("invalid segment timestamp: epoch or zero timestamp")
var _ Cache = (*segmentCache)(nil)
@@ -482,61 +480,50 @@ func (sc *segmentController[T, O]) parse(value string) (time.Time, error) {
func (sc *segmentController[T, O]) open() error {
sc.Lock()
defer sc.Unlock()
- invalidSegments := make([]string, 0)
+ emptySegments := make([]string, 0)
err := loadSegments(sc.location, segPathPrefix, sc, sc.getOptions().SegmentInterval, func(start, end time.Time) error {
suffix := sc.format(start)
segmentPath := path.Join(sc.location, fmt.Sprintf(segTemplate, suffix))
// Detect epoch/near-epoch segments created by zero MinTimestamp in sync paths.
// BanyanDB is an APM database -- no legitimate data predates the year 2000.
if start.UnixNano() <= 0 {
- invalidSegments = append(invalidSegments, segmentPath)
+ emptySegments = append(emptySegments, segmentPath)
return nil
}
metadataPath := path.Join(segmentPath, metadataFilename)
- rawMeta, readErr := sc.lfs.Read(metadataPath)
- if readErr != nil {
- if errors.Is(readErr, fs.ErrNotExist) {
- invalidSegments = append(invalidSegments, segmentPath)
+ version, err := sc.lfs.Read(metadataPath)
+ if err != nil {
+ if errors.Is(err, fs.ErrNotExist) {
+ emptySegments = append(emptySegments, segmentPath)
return nil
}
- return readErr
+ return err
}
- if len(rawMeta) == 0 {
- invalidSegments = append(invalidSegments, segmentPath)
+ if len(version) == 0 {
+ emptySegments = append(emptySegments, segmentPath)
return nil
}
- meta, parseErr := readSegmentMeta(rawMeta)
- if parseErr != nil {
- return parseErr
- }
- segmentEnd := end
- if meta.EndTime != "" {
- parsedEnd, timeErr := time.Parse(time.RFC3339Nano, meta.EndTime)
- if timeErr != nil {
- return timeErr
- }
- segmentEnd = parsedEnd
+ if err = checkVersion(convert.BytesToString(version)); err != nil {
+ return err
}
- _, loadErr := sc.load(start, segmentEnd, sc.location)
- return loadErr
+ _, err = sc.load(start, end, sc.location)
+ return err
})
- if len(invalidSegments) > 0 {
- sc.l.Warn().Strs("segments", invalidSegments).Msg("invalid segments found (empty or epoch-dated), removing them")
- for i := range invalidSegments {
- sc.lfs.MustRMAll(invalidSegments[i])
+ if len(emptySegments) > 0 {
+ sc.l.Warn().Strs("segments", emptySegments).Msg("empty segments found, removing them.")
+ for i := range emptySegments {
+ sc.lfs.MustRMAll(emptySegments[i])
}
}
return err
}
func (sc *segmentController[T, O]) create(start time.Time) (*segment[T, O], error) {
- // Reject epoch/near-epoch timestamps caused by zero MinTimestamp flowing through sync paths.
- // BanyanDB is an APM database -- no legitimate data predates the year 2000.
+ sc.Lock()
+ defer sc.Unlock()
if start.UnixNano() <= 0 {
return nil, ErrInvalidSegmentTimestamp
}
- sc.Lock()
- defer sc.Unlock()
last := len(sc.lst) - 1
for i := range sc.lst {
s := sc.lst[last-i]
@@ -564,14 +551,7 @@ func (sc *segmentController[T, O]) create(start time.Time) (*segment[T, O], erro
}
segPath := path.Join(sc.location, fmt.Sprintf(segTemplate, sc.format(start)))
sc.lfs.MkdirPanicIfExist(segPath, DirPerm)
- meta := segmentMeta{
- Version: currentVersion,
- EndTime: end.Format(time.RFC3339Nano),
- }
- data, marshalErr := json.Marshal(meta)
- if marshalErr != nil {
- logger.Panicf("cannot marshal segment metadata: %s", marshalErr)
- }
+ data := []byte(currentVersion)
metadataPath := filepath.Join(segPath, metadataFilename)
lf, err := sc.lfs.CreateLockFile(metadataPath, FilePerm)
if err != nil {
diff --git a/banyand/trace/merger_test.go b/banyand/trace/merger_test.go
index f091cd166..f162b400e 100644
--- a/banyand/trace/merger_test.go
+++ b/banyand/trace/merger_test.go
@@ -1456,420 +1456,3 @@ func Test_flush_cleansUpOnSidxFlushError(t *testing.T) {
sidxPartPath := filepath.Join(tmpPath, sidxDirName, "idx1", fmt.Sprintf("%016x", partID))
require.False(t, fileSystem.IsExist(sidxPartPath), "sidx part directory should be removed on sidx flush failure")
}
-
-func Test_computeSmallMergeThreshold(t *testing.T) {
- threshold := computeSmallMergeThreshold()
- require.GreaterOrEqual(t, threshold, uint64(defaultSmallMergeThreshold),
- "threshold should be at least the default (%d bytes)", defaultSmallMergeThreshold)
-}
-
-func Test_getPartsToMerge_skipsInFlightParts(t *testing.T) {
- tmpPath, defFn := test.Space(require.New(t))
- defer defFn()
-
- fileSystem := fs.NewLocalFileSystem()
-
- // Create 4 file parts
- var allParts []*partWrapper
- for i := range 4 {
- partID := uint64(100 + i)
- mp := generateMemPart()
- mp.mustInitFromTraces(tsTS1)
- mp.mustFlush(fileSystem, partPath(tmpPath, partID))
- p := mustOpenFilePart(partID, tmpPath, fileSystem)
- p.partMetadata.ID = partID
- allParts = append(allParts, newPartWrapper(nil, p))
- releaseMemPart(mp)
- }
-
- closer := run.NewCloser(1)
- defer closer.Done()
-
- tst := &tsTable{
- pm: protector.Nop{},
- fileSystem: fileSystem,
- root: tmpPath,
- loopCloser: closer,
- l: logger.GetLogger("test"),
- curPartID: 200,
- option: option{
- mergePolicy: newDefaultMergePolicyForTesting(),
- },
- }
-
- snp := &snapshot{
- parts: allParts,
- epoch: 1,
- }
- snp.incRef()
- defer snp.decRef()
-
- // Without inFlight: all parts should be eligible
- dst, _ := tst.getPartsToMerge(snp, uint64(1<<30), nil)
- require.GreaterOrEqual(t, len(dst), 2, "should find parts to merge when none are in-flight")
-
- // Mark part 100 and 101 as in-flight
- tst.inFlight = make(map[uint64]struct{})
- tst.inFlight[100] = struct{}{}
- tst.inFlight[101] = struct{}{}
-
- // With inFlight: parts 100 and 101 should be skipped
- dst2, _ := tst.getPartsToMerge(snp, uint64(1<<30), nil)
- if len(dst2) > 0 {
- for _, pw := range dst2 {
- require.NotEqual(t, uint64(100), pw.ID(), "part 100 should be skipped (in-flight)")
- require.NotEqual(t, uint64(101), pw.ID(), "part 101 should be skipped (in-flight)")
- }
- }
-
- // Mark all parts as in-flight: no parts should be eligible
- tst.inFlight[102] = struct{}{}
- tst.inFlight[103] = struct{}{}
- dst3, _ := tst.getPartsToMerge(snp, uint64(1<<30), nil)
- require.Nil(t, dst3, "no parts should be eligible when all are in-flight")
-}
-
-func Test_dispatchAllMerges_laneClassification(t *testing.T) {
- tmpPath, defFn := test.Space(require.New(t))
- defer defFn()
-
- fileSystem := fs.NewLocalFileSystem()
-
- closer := run.NewCloser(1)
-
- tst := &tsTable{
- pm: protector.Nop{},
- fileSystem: fileSystem,
- root: tmpPath,
- loopCloser: closer,
- l: logger.GetLogger("test"),
- curPartID: 200,
- option: option{
- mergePolicy: newDefaultMergePolicyForTesting(),
- },
- }
-
- // Use a very low threshold to force all merges into slow lane
- smallThreshold := uint64(1) // 1 byte - everything is "large"
- slowCh := make(chan *mergeDispatchRequest, 10)
- fastCh := make(chan *mergeDispatchRequest, 10)
-
- // Create 3 file parts and build a snapshot
- var allParts []*partWrapper
- for i := range 3 {
- partID := uint64(200 + i)
- mp := generateMemPart()
- mp.mustInitFromTraces(tsTS1)
- mp.mustFlush(fileSystem, partPath(tmpPath, partID))
- p := mustOpenFilePart(partID, tmpPath, fileSystem)
- p.partMetadata.ID = partID
- allParts = append(allParts, newPartWrapper(nil, p))
- releaseMemPart(mp)
- }
-
- snp := &snapshot{
- parts: allParts,
- epoch: 1,
- }
- snp.incRef()
- tst.snapshot = snp
- defer snp.decRef()
-
- // Drain introductions
- merges := make(chan *mergerIntroduction, 10)
- go func() {
- for m := range merges {
- _ = m
- }
- }()
-
- // Run dispatchAllMerges - it should dispatch to slowCh since threshold is 1 byte
- dispatchDone := make(chan struct{})
- go func() {
- defer close(dispatchDone)
- tst.dispatchAllMerges(smallThreshold, fastCh, slowCh)
- }()
-
- // Should receive on slowCh
- select {
- case req := <-slowCh:
- require.Equal(t, mergeTypeFile, req.typ)
- require.Equal(t, mergeLaneSlow, req.lane)
- require.GreaterOrEqual(t, len(req.parts), 2, "should merge at least 2 parts")
- // Verify inFlight was set
- tst.inFlightMu.RLock()
- for _, pw := range req.parts {
- _, exists := tst.inFlight[pw.ID()]
- require.True(t, exists, "part %d should be in inFlight map", pw.ID())
- }
- tst.inFlightMu.RUnlock()
- // Don't clear inFlight — dispatchAllMerges loop will find no eligible parts and exit naturally
- case <-fastCh:
- t.Fatal("should not dispatch to fast lane with threshold=1")
- case <-time.After(5 * time.Second):
- t.Fatal("timed out waiting for dispatch")
- }
-
- // Wait for dispatchAllMerges to finish (it exits when all parts are in inFlight)
- select {
- case <-dispatchDone:
- case <-time.After(5 * time.Second):
- t.Fatal("dispatchAllMerges did not finish")
- }
-
- // Close the closer after dispatchAllMerges has exited
- closer.Done()
- closer.CloseThenWait()
- close(fastCh)
- close(slowCh)
- close(merges)
-}
-
-func Test_mergeLaneWorker_cleansUpInFlight(t *testing.T) {
- tmpPath, defFn := test.Space(require.New(t))
- defer defFn()
-
- fileSystem := fs.NewLocalFileSystem()
-
- closer := run.NewCloser(1)
-
- tst := &tsTable{
- pm: protector.Nop{},
- fileSystem: fileSystem,
- root: tmpPath,
- loopCloser: closer,
- l: logger.GetLogger("test"),
- curPartID: 0,
- option: option{
- mergePolicy: newDefaultMergePolicyForTesting(),
- },
- inFlight: make(map[uint64]struct{}),
- }
-
- // Create 2 file parts to merge
- var parts []*partWrapper
- for i := range 2 {
- partID := uint64(300 + i)
- mp := generateMemPart()
- mp.mustInitFromTraces(tsTS1)
- mp.mustFlush(fileSystem, partPath(tmpPath, partID))
- p := mustOpenFilePart(partID, tmpPath, fileSystem)
- p.partMetadata.ID = partID
- parts = append(parts, newPartWrapper(nil, p))
- releaseMemPart(mp)
- }
-
- // Mark parts as inFlight
- toBeMerged := make(map[uint64]struct{})
- for _, pw := range parts {
- tst.inFlight[pw.ID()] = struct{}{}
- toBeMerged[pw.ID()] = struct{}{}
- }
-
- require.Len(t, tst.inFlight, 2)
-
- // Create channels
- ch := make(chan *mergeDispatchRequest, 1)
- merges := make(chan *mergerIntroduction, 1)
-
- // Start a mock introducer to consume from merges and close applied.
- mockIntroducerDone := make(chan struct{})
- go func() {
- defer close(mockIntroducerDone)
- for mi := range merges {
- if mi.newPart != nil {
- mi.newPart.decRef()
- }
- if mi.applied != nil {
- close(mi.applied)
- }
- }
- }()
-
- // Start worker in background
- workerDone := make(chan struct{})
- go func() {
- defer close(workerDone)
- tst.mergeLaneWorker(ch, merges)
- }()
-
- // Send a dispatch request
- ch <- &mergeDispatchRequest{
- parts: parts,
- toBeMerged: toBeMerged,
- typ: mergeTypeFile,
- lane: mergeLaneFast,
- }
- close(ch)
-
- // Close the closer to signal shutdown.
- time.Sleep(100 * time.Millisecond)
- closer.Done()
- closer.CloseThenWait()
-
- // Wait for worker to finish
- select {
- case <-workerDone:
- case <-time.After(5 * time.Second):
- t.Fatal("worker did not finish")
- }
- close(merges)
- <-mockIntroducerDone
-
- // Verify inFlight was cleaned up
- tst.inFlightMu.RLock()
- require.Empty(t, tst.inFlight, "inFlight map should be empty after worker completes")
- tst.inFlightMu.RUnlock()
-}
-
-func Test_mergePartsWithConflictingTagTypes(t *testing.T) {
- tests := []struct {
- name string
- parts []*traces
- wantTagTypes map[string]pbv1.ValueType
- wantDeleted []string
- }{
- {
- name: "multiple parts with int and str for same tag",
- parts: []*traces{
- {
- traceIDs: []string{"trace1"},
- timestamps: []int64{1},
- tags: [][]*tagValue{
- {
- {tag: "state", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(100)},
- {tag: "name", valueType: pbv1.ValueTypeStr, value: []byte("svc1")},
- },
- },
- spans: [][]byte{[]byte("span1")},
- spanIDs: []string{"span1"},
- },
- {
- traceIDs: []string{"trace2"},
- timestamps: []int64{2},
- tags: [][]*tagValue{
- {
- {tag: "state", valueType: pbv1.ValueTypeStr, value: []byte("active")},
- {tag: "name", valueType: pbv1.ValueTypeStr, value: []byte("svc2")},
- },
- },
- spans: [][]byte{[]byte("span2")},
- spanIDs: []string{"span2"},
- },
- },
- wantTagTypes: map[string]pbv1.ValueType{
- "state#int": pbv1.ValueTypeInt64,
- "state#str": pbv1.ValueTypeStr,
- "name": pbv1.ValueTypeStr,
- },
- wantDeleted: []string{"state"},
- },
- {
- name: "merge typed-suffix part with plain part",
- parts: []*traces{
- {
- traceIDs: []string{"trace1"},
- timestamps: []int64{1},
- tags: [][]*tagValue{
- {{tag: "state#int", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(100)}},
- },
- spans: [][]byte{[]byte("span1")},
- spanIDs: []string{"span1"},
- },
- {
- traceIDs: []string{"trace2"},
- timestamps: []int64{2},
- tags: [][]*tagValue{
- {{tag: "state", valueType: pbv1.ValueTypeStr, value: []byte("pending")}},
- },
- spans: [][]byte{[]byte("span2")},
- spanIDs: []string{"span2"},
- },
- },
- wantTagTypes: map[string]pbv1.ValueType{
- "state#int": pbv1.ValueTypeInt64,
- "state#str": pbv1.ValueTypeStr,
- },
- wantDeleted: []string{"state"},
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- tmpPath, defFn := test.Space(require.New(t))
- defer defFn()
- fileSystem := fs.NewLocalFileSystem()
-
- partIDs := make([]uint64, len(tt.parts))
- for idx := range partIDs {
- partIDs[idx] = uint64(idx)
- }
- dstID := uint64(999)
- dstPath := mergePartsWithConflictTags(t, fileSystem, tmpPath, tt.parts, partIDs, dstID)
-
- mergedPart := mustOpenFilePart(dstID, tmpPath, fileSystem)
- for tag, wantType := range tt.wantTagTypes {
- gotType, exists := mergedPart.tagType[tag]
- require.True(t, exists, "expected tag type entry for %q", tag)
- require.Equal(t, wantType, gotType, "tag type mismatch for %q", tag)
- }
- for _, tag := range tt.wantDeleted {
- _, ok := mergedPart.tagType[tag]
- require.False(t, ok, "tag %q should not exist in tagType after merge", tag)
- dataFile := filepath.Join(dstPath, tag+tagsFilenameExt)
- require.False(t, fileSystem.IsExist(dataFile), "tag data file %q should not exist", dataFile)
- metaFile := filepath.Join(dstPath, tag+tagsMetadataFilenameExt)
- require.False(t, fileSystem.IsExist(metaFile), "tag metadata file %q should not exist", metaFile)
- }
- })
- }
-}
-
-func mergePartsWithConflictTags(t *testing.T, fileSystem fs.FileSystem, tmpPath string, tsList []*traces, partIDs []uint64, dstID uint64) string {
- t.Helper()
- var parts []*partWrapper
- for i, traces := range tsList {
- mp := generateMemPart()
- mp.mustInitFromTraces(traces)
- mp.mustFlush(fileSystem, partPath(tmpPath, partIDs[i]))
- p := mustOpenFilePart(partIDs[i], tmpPath, fileSystem)
- parts = append(parts, &partWrapper{p: p})
- releaseMemPart(mp)
- }
-
- conflictTags := collectConflictTags(parts)
-
- var pmi []*partMergeIter
- var traceSize uint64
- for i := range tsList {
- p := mustOpenFilePart(partIDs[i], tmpPath, fileSystem)
- iter := generatePartMergeIter()
- iter.mustInitFromPart(p)
- pmi = append(pmi, iter)
- traceSize += uint64(len(tsList[i].traceIDs))
- }
-
- br := generateBlockReader()
- br.init(pmi)
- bw := generateBlockWriter()
- dstPath := partPath(tmpPath, dstID)
- bw.mustInitForFilePart(fileSystem, dstPath, false, int(traceSize))
-
- closeCh := make(chan struct{})
- pm, tf, tagTypes, mergeErr := mergeBlocks(closeCh, bw, br, conflictTags)
- close(closeCh)
- require.NoError(t, mergeErr)
- require.NotNil(t, pm)
-
- releaseBlockWriter(bw)
- releaseBlockReader(br)
- for _, iter := range pmi {
- releasePartMergeIter(iter)
- }
-
- pm.mustWriteMetadata(fileSystem, dstPath)
- tf.mustWriteTraceIDFilter(fileSystem, dstPath)
- tagTypes.mustWriteTagType(fileSystem, dstPath)
- fileSystem.SyncPath(dstPath)
- return dstPath
-}
(skywalking-banyandb) 19/24: fix(measure,stream,trace): eliminate flusher/introducer data race on shutdown (#1094)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 74d9f7409ed6b48ac98fb75f519318ae49140b30
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Mon Apr 27 14:27:38 2026 +0800
fix(measure,stream,trace): eliminate flusher/introducer data race on shutdown (#1094)
---
banyand/measure/flusher.go | 5 +-
banyand/measure/introducer.go | 39 +++-
banyand/measure/merger.go | 6 +-
banyand/measure/syncer.go | 6 +-
banyand/measure/tstable.go | 10 +-
banyand/stream/flusher.go | 5 +-
banyand/stream/introducer.go | 39 +++-
banyand/stream/merger.go | 6 +-
banyand/stream/syncer.go | 6 +-
banyand/stream/tstable.go | 10 +-
banyand/trace/flusher.go | 5 +-
banyand/trace/introducer.go | 39 +++-
banyand/trace/merger.go | 6 +-
banyand/trace/merger_test.go | 417 ++++++++++++++++++++++++++++++++++++++++++
banyand/trace/syncer.go | 6 +-
banyand/trace/tstable.go | 10 +-
16 files changed, 543 insertions(+), 72 deletions(-)
diff --git a/banyand/measure/flusher.go b/banyand/measure/flusher.go
index c0390f50b..38c01d751 100644
--- a/banyand/measure/flusher.go
+++ b/banyand/measure/flusher.go
@@ -204,10 +204,7 @@ func (tst *tsTable) flush(snapshot *snapshot, flushCh chan *flusherIntroduction)
case <-tst.loopCloser.CloseNotify():
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
tst.incTotalFlushIntroLatency(time.Since(end).Seconds())
}
diff --git a/banyand/measure/introducer.go b/banyand/measure/introducer.go
index 92dc02c4e..53fb8df5b 100644
--- a/banyand/measure/introducer.go
+++ b/banyand/measure/introducer.go
@@ -117,7 +117,23 @@ func (tst *tsTable) introducerLoop(flushCh chan *flusherIntroduction, mergeCh ch
for {
select {
case <-tst.loopCloser.CloseNotify():
- return
+ // Drain pending introductions so senders don't block on applied.
+ for {
+ select {
+ case next := <-tst.introductions:
+ tst.introducePart(next, epoch)
+ epoch++
+ case next := <-flushCh:
+ tst.introduceFlushed(next, epoch)
+ epoch++
+ case next := <-mergeCh:
+ tst.introduceMerged(next, epoch)
+ epoch++
+ default:
+ tst.gc.clean()
+ return
+ }
+ }
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
tst.introducePart(next, epoch)
@@ -151,7 +167,26 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
for {
select {
case <-tst.loopCloser.CloseNotify():
- return
+ // Drain pending introductions so senders don't block on applied.
+ for {
+ select {
+ case next := <-tst.introductions:
+ tst.introducePart(next, epoch)
+ epoch++
+ case next := <-flushCh:
+ tst.introduceFlushed(next, epoch)
+ epoch++
+ case next := <-mergeCh:
+ tst.introduceMerged(next, epoch)
+ epoch++
+ case next := <-syncCh:
+ tst.introduceSync(next, epoch)
+ epoch++
+ default:
+ tst.gc.clean()
+ return
+ }
+ }
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
tst.introducePart(next, epoch)
diff --git a/banyand/measure/merger.go b/banyand/measure/merger.go
index ef359288c..206dee25d 100644
--- a/banyand/measure/merger.go
+++ b/banyand/measure/merger.go
@@ -163,11 +163,7 @@ func (tst *tsTable) mergePartsThenSendIntroduction(creator snapshotCreator, part
case <-tst.loopCloser.CloseNotify():
return newPart, errClosed
}
- select {
- case <-mi.applied:
- case <-tst.loopCloser.CloseNotify():
- return newPart, errClosed
- }
+ <-mi.applied
return newPart, nil
}
diff --git a/banyand/measure/syncer.go b/banyand/measure/syncer.go
index 7b06e0c19..a62f42870 100644
--- a/banyand/measure/syncer.go
+++ b/banyand/measure/syncer.go
@@ -466,11 +466,7 @@ func (tst *tsTable) sendSyncIntroduction(partsToSync []*part, syncCh chan *syncI
case <-tst.loopCloser.CloseNotify():
return errClosed
}
- select {
- case <-si.applied:
- case <-tst.loopCloser.CloseNotify():
- return errClosed
- }
+ <-si.applied
return nil
}
diff --git a/banyand/measure/tstable.go b/banyand/measure/tstable.go
index 6c3fff58e..34e989601 100644
--- a/banyand/measure/tstable.go
+++ b/banyand/measure/tstable.go
@@ -364,10 +364,7 @@ func (tst *tsTable) mustAddFilePart(partID uint64) {
ind.part.decRef()
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
}
func (tst *tsTable) mustAddMemPart(mp *memPart) {
@@ -388,10 +385,7 @@ func (tst *tsTable) mustAddMemPart(mp *memPart) {
ind.part.decRef()
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
tst.incTotalWritten(int(totalCount))
tst.incTotalBatch(1)
tst.incTotalBatchIntroLatency(time.Since(startTime).Seconds())
diff --git a/banyand/stream/flusher.go b/banyand/stream/flusher.go
index 04788af0c..f2c7f0742 100644
--- a/banyand/stream/flusher.go
+++ b/banyand/stream/flusher.go
@@ -234,10 +234,7 @@ func (tst *tsTable) flush(snapshot *snapshot, flushCh chan *flusherIntroduction)
case <-tst.loopCloser.CloseNotify():
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
tst.incTotalFlushIntroLatency(time.Since(end).Seconds())
}
diff --git a/banyand/stream/introducer.go b/banyand/stream/introducer.go
index e2255578d..621ea4238 100644
--- a/banyand/stream/introducer.go
+++ b/banyand/stream/introducer.go
@@ -146,7 +146,23 @@ func (tst *tsTable) introducerLoop(flushCh chan *flusherIntroduction, mergeCh ch
for {
select {
case <-tst.loopCloser.CloseNotify():
- return
+ // Drain pending introductions so senders don't block on applied.
+ for {
+ select {
+ case next := <-tst.introductions:
+ tst.introducePart(next, epoch)
+ epoch++
+ case next := <-flushCh:
+ tst.introduceFlushed(next, epoch)
+ epoch++
+ case next := <-mergeCh:
+ tst.introduceMerged(next, epoch)
+ epoch++
+ default:
+ tst.gc.clean()
+ return
+ }
+ }
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
tst.introducePart(next, epoch)
@@ -180,7 +196,26 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
for {
select {
case <-tst.loopCloser.CloseNotify():
- return
+ // Drain pending introductions so senders don't block on applied.
+ for {
+ select {
+ case next := <-tst.introductions:
+ tst.introducePart(next, epoch)
+ epoch++
+ case next := <-flushCh:
+ tst.introduceFlushed(next, epoch)
+ epoch++
+ case next := <-mergeCh:
+ tst.introduceMerged(next, epoch)
+ epoch++
+ case next := <-syncCh:
+ tst.introduceSync(next, epoch)
+ epoch++
+ default:
+ tst.gc.clean()
+ return
+ }
+ }
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
tst.introducePart(next, epoch)
diff --git a/banyand/stream/merger.go b/banyand/stream/merger.go
index 2646c0b8b..ba7486878 100644
--- a/banyand/stream/merger.go
+++ b/banyand/stream/merger.go
@@ -162,11 +162,7 @@ func (tst *tsTable) mergePartsThenSendIntroduction(creator snapshotCreator, part
case <-tst.loopCloser.CloseNotify():
return newPart, errClosed
}
- select {
- case <-mi.applied:
- case <-tst.loopCloser.CloseNotify():
- return newPart, errClosed
- }
+ <-mi.applied
return newPart, nil
}
diff --git a/banyand/stream/syncer.go b/banyand/stream/syncer.go
index 37d3040c9..55c39c1b4 100644
--- a/banyand/stream/syncer.go
+++ b/banyand/stream/syncer.go
@@ -439,11 +439,7 @@ func (tst *tsTable) sendSyncIntroduction(partsToSync []*part, syncCh chan *syncI
case <-tst.loopCloser.CloseNotify():
return errClosed
}
- select {
- case <-si.applied:
- case <-tst.loopCloser.CloseNotify():
- return errClosed
- }
+ <-si.applied
return nil
}
diff --git a/banyand/stream/tstable.go b/banyand/stream/tstable.go
index 9f3f5e7a0..40f72581f 100644
--- a/banyand/stream/tstable.go
+++ b/banyand/stream/tstable.go
@@ -375,10 +375,7 @@ func (tst *tsTable) mustAddFilePart(partID uint64) {
ind.part.decRef()
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
}
func (tst *tsTable) mustAddMemPart(mp *memPart) {
@@ -399,10 +396,7 @@ func (tst *tsTable) mustAddMemPart(mp *memPart) {
ind.part.decRef()
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
tst.incTotalWritten(int(totalCount))
tst.incTotalBatch(1)
tst.incTotalBatchIntroLatency(time.Since(startTime).Seconds())
diff --git a/banyand/trace/flusher.go b/banyand/trace/flusher.go
index 8ebe7baf0..e82645476 100644
--- a/banyand/trace/flusher.go
+++ b/banyand/trace/flusher.go
@@ -273,10 +273,7 @@ func (tst *tsTable) flush(snapshot *snapshot, flushCh chan *flusherIntroduction)
}
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
tst.incTotalFlushIntroLatency(time.Since(end).Seconds())
}
diff --git a/banyand/trace/introducer.go b/banyand/trace/introducer.go
index 93e23da08..b72047fd0 100644
--- a/banyand/trace/introducer.go
+++ b/banyand/trace/introducer.go
@@ -159,7 +159,23 @@ func (tst *tsTable) introducerLoop(flushCh chan *flusherIntroduction, mergeCh ch
for {
select {
case <-tst.loopCloser.CloseNotify():
- return
+ // Drain pending introductions so senders don't block on applied.
+ for {
+ select {
+ case next := <-tst.introductions:
+ tst.introducePart(next, epoch)
+ epoch++
+ case next := <-flushCh:
+ tst.introduceFlushed(next, epoch)
+ epoch++
+ case next := <-mergeCh:
+ tst.introduceMerged(next, epoch)
+ epoch++
+ default:
+ tst.gc.clean()
+ return
+ }
+ }
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
tst.introducePart(next, epoch)
@@ -193,7 +209,26 @@ func (tst *tsTable) introducerLoopWithSync(flushCh chan *flusherIntroduction, me
for {
select {
case <-tst.loopCloser.CloseNotify():
- return
+ // Drain pending introductions so senders don't block on applied.
+ for {
+ select {
+ case next := <-tst.introductions:
+ tst.introducePart(next, epoch)
+ epoch++
+ case next := <-flushCh:
+ tst.introduceFlushedForSync(next, epoch)
+ epoch++
+ case next := <-mergeCh:
+ tst.introduceMerged(next, epoch)
+ epoch++
+ case next := <-syncCh:
+ tst.introduceSync(next, epoch)
+ epoch++
+ default:
+ tst.gc.clean()
+ return
+ }
+ }
case next := <-tst.introductions:
tst.incTotalIntroduceLoopStarted("mem")
tst.introducePart(next, epoch)
diff --git a/banyand/trace/merger.go b/banyand/trace/merger.go
index 875f98fb8..480897746 100644
--- a/banyand/trace/merger.go
+++ b/banyand/trace/merger.go
@@ -209,11 +209,7 @@ func (tst *tsTable) mergePartsThenSendIntroduction(creator snapshotCreator, part
case <-tst.loopCloser.CloseNotify():
return newPart, errClosed
}
- select {
- case <-mi.applied:
- case <-tst.loopCloser.CloseNotify():
- return newPart, errClosed
- }
+ <-mi.applied
return newPart, nil
}
diff --git a/banyand/trace/merger_test.go b/banyand/trace/merger_test.go
index f162b400e..f091cd166 100644
--- a/banyand/trace/merger_test.go
+++ b/banyand/trace/merger_test.go
@@ -1456,3 +1456,420 @@ func Test_flush_cleansUpOnSidxFlushError(t *testing.T) {
sidxPartPath := filepath.Join(tmpPath, sidxDirName, "idx1", fmt.Sprintf("%016x", partID))
require.False(t, fileSystem.IsExist(sidxPartPath), "sidx part directory should be removed on sidx flush failure")
}
+
+func Test_computeSmallMergeThreshold(t *testing.T) {
+ threshold := computeSmallMergeThreshold()
+ require.GreaterOrEqual(t, threshold, uint64(defaultSmallMergeThreshold),
+ "threshold should be at least the default (%d bytes)", defaultSmallMergeThreshold)
+}
+
+func Test_getPartsToMerge_skipsInFlightParts(t *testing.T) {
+ tmpPath, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+
+ // Create 4 file parts
+ var allParts []*partWrapper
+ for i := range 4 {
+ partID := uint64(100 + i)
+ mp := generateMemPart()
+ mp.mustInitFromTraces(tsTS1)
+ mp.mustFlush(fileSystem, partPath(tmpPath, partID))
+ p := mustOpenFilePart(partID, tmpPath, fileSystem)
+ p.partMetadata.ID = partID
+ allParts = append(allParts, newPartWrapper(nil, p))
+ releaseMemPart(mp)
+ }
+
+ closer := run.NewCloser(1)
+ defer closer.Done()
+
+ tst := &tsTable{
+ pm: protector.Nop{},
+ fileSystem: fileSystem,
+ root: tmpPath,
+ loopCloser: closer,
+ l: logger.GetLogger("test"),
+ curPartID: 200,
+ option: option{
+ mergePolicy: newDefaultMergePolicyForTesting(),
+ },
+ }
+
+ snp := &snapshot{
+ parts: allParts,
+ epoch: 1,
+ }
+ snp.incRef()
+ defer snp.decRef()
+
+ // Without inFlight: all parts should be eligible
+ dst, _ := tst.getPartsToMerge(snp, uint64(1<<30), nil)
+ require.GreaterOrEqual(t, len(dst), 2, "should find parts to merge when none are in-flight")
+
+ // Mark part 100 and 101 as in-flight
+ tst.inFlight = make(map[uint64]struct{})
+ tst.inFlight[100] = struct{}{}
+ tst.inFlight[101] = struct{}{}
+
+ // With inFlight: parts 100 and 101 should be skipped
+ dst2, _ := tst.getPartsToMerge(snp, uint64(1<<30), nil)
+ if len(dst2) > 0 {
+ for _, pw := range dst2 {
+ require.NotEqual(t, uint64(100), pw.ID(), "part 100 should be skipped (in-flight)")
+ require.NotEqual(t, uint64(101), pw.ID(), "part 101 should be skipped (in-flight)")
+ }
+ }
+
+ // Mark all parts as in-flight: no parts should be eligible
+ tst.inFlight[102] = struct{}{}
+ tst.inFlight[103] = struct{}{}
+ dst3, _ := tst.getPartsToMerge(snp, uint64(1<<30), nil)
+ require.Nil(t, dst3, "no parts should be eligible when all are in-flight")
+}
+
+func Test_dispatchAllMerges_laneClassification(t *testing.T) {
+ tmpPath, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+
+ closer := run.NewCloser(1)
+
+ tst := &tsTable{
+ pm: protector.Nop{},
+ fileSystem: fileSystem,
+ root: tmpPath,
+ loopCloser: closer,
+ l: logger.GetLogger("test"),
+ curPartID: 200,
+ option: option{
+ mergePolicy: newDefaultMergePolicyForTesting(),
+ },
+ }
+
+ // Use a very low threshold to force all merges into slow lane
+ smallThreshold := uint64(1) // 1 byte - everything is "large"
+ slowCh := make(chan *mergeDispatchRequest, 10)
+ fastCh := make(chan *mergeDispatchRequest, 10)
+
+ // Create 3 file parts and build a snapshot
+ var allParts []*partWrapper
+ for i := range 3 {
+ partID := uint64(200 + i)
+ mp := generateMemPart()
+ mp.mustInitFromTraces(tsTS1)
+ mp.mustFlush(fileSystem, partPath(tmpPath, partID))
+ p := mustOpenFilePart(partID, tmpPath, fileSystem)
+ p.partMetadata.ID = partID
+ allParts = append(allParts, newPartWrapper(nil, p))
+ releaseMemPart(mp)
+ }
+
+ snp := &snapshot{
+ parts: allParts,
+ epoch: 1,
+ }
+ snp.incRef()
+ tst.snapshot = snp
+ defer snp.decRef()
+
+ // Drain introductions
+ merges := make(chan *mergerIntroduction, 10)
+ go func() {
+ for m := range merges {
+ _ = m
+ }
+ }()
+
+ // Run dispatchAllMerges - it should dispatch to slowCh since threshold is 1 byte
+ dispatchDone := make(chan struct{})
+ go func() {
+ defer close(dispatchDone)
+ tst.dispatchAllMerges(smallThreshold, fastCh, slowCh)
+ }()
+
+ // Should receive on slowCh
+ select {
+ case req := <-slowCh:
+ require.Equal(t, mergeTypeFile, req.typ)
+ require.Equal(t, mergeLaneSlow, req.lane)
+ require.GreaterOrEqual(t, len(req.parts), 2, "should merge at least 2 parts")
+ // Verify inFlight was set
+ tst.inFlightMu.RLock()
+ for _, pw := range req.parts {
+ _, exists := tst.inFlight[pw.ID()]
+ require.True(t, exists, "part %d should be in inFlight map", pw.ID())
+ }
+ tst.inFlightMu.RUnlock()
+ // Don't clear inFlight — dispatchAllMerges loop will find no eligible parts and exit naturally
+ case <-fastCh:
+ t.Fatal("should not dispatch to fast lane with threshold=1")
+ case <-time.After(5 * time.Second):
+ t.Fatal("timed out waiting for dispatch")
+ }
+
+ // Wait for dispatchAllMerges to finish (it exits when all parts are in inFlight)
+ select {
+ case <-dispatchDone:
+ case <-time.After(5 * time.Second):
+ t.Fatal("dispatchAllMerges did not finish")
+ }
+
+ // Close the closer after dispatchAllMerges has exited
+ closer.Done()
+ closer.CloseThenWait()
+ close(fastCh)
+ close(slowCh)
+ close(merges)
+}
+
+func Test_mergeLaneWorker_cleansUpInFlight(t *testing.T) {
+ tmpPath, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ fileSystem := fs.NewLocalFileSystem()
+
+ closer := run.NewCloser(1)
+
+ tst := &tsTable{
+ pm: protector.Nop{},
+ fileSystem: fileSystem,
+ root: tmpPath,
+ loopCloser: closer,
+ l: logger.GetLogger("test"),
+ curPartID: 0,
+ option: option{
+ mergePolicy: newDefaultMergePolicyForTesting(),
+ },
+ inFlight: make(map[uint64]struct{}),
+ }
+
+ // Create 2 file parts to merge
+ var parts []*partWrapper
+ for i := range 2 {
+ partID := uint64(300 + i)
+ mp := generateMemPart()
+ mp.mustInitFromTraces(tsTS1)
+ mp.mustFlush(fileSystem, partPath(tmpPath, partID))
+ p := mustOpenFilePart(partID, tmpPath, fileSystem)
+ p.partMetadata.ID = partID
+ parts = append(parts, newPartWrapper(nil, p))
+ releaseMemPart(mp)
+ }
+
+ // Mark parts as inFlight
+ toBeMerged := make(map[uint64]struct{})
+ for _, pw := range parts {
+ tst.inFlight[pw.ID()] = struct{}{}
+ toBeMerged[pw.ID()] = struct{}{}
+ }
+
+ require.Len(t, tst.inFlight, 2)
+
+ // Create channels
+ ch := make(chan *mergeDispatchRequest, 1)
+ merges := make(chan *mergerIntroduction, 1)
+
+ // Start a mock introducer to consume from merges and close applied.
+ mockIntroducerDone := make(chan struct{})
+ go func() {
+ defer close(mockIntroducerDone)
+ for mi := range merges {
+ if mi.newPart != nil {
+ mi.newPart.decRef()
+ }
+ if mi.applied != nil {
+ close(mi.applied)
+ }
+ }
+ }()
+
+ // Start worker in background
+ workerDone := make(chan struct{})
+ go func() {
+ defer close(workerDone)
+ tst.mergeLaneWorker(ch, merges)
+ }()
+
+ // Send a dispatch request
+ ch <- &mergeDispatchRequest{
+ parts: parts,
+ toBeMerged: toBeMerged,
+ typ: mergeTypeFile,
+ lane: mergeLaneFast,
+ }
+ close(ch)
+
+ // Close the closer to signal shutdown.
+ time.Sleep(100 * time.Millisecond)
+ closer.Done()
+ closer.CloseThenWait()
+
+ // Wait for worker to finish
+ select {
+ case <-workerDone:
+ case <-time.After(5 * time.Second):
+ t.Fatal("worker did not finish")
+ }
+ close(merges)
+ <-mockIntroducerDone
+
+ // Verify inFlight was cleaned up
+ tst.inFlightMu.RLock()
+ require.Empty(t, tst.inFlight, "inFlight map should be empty after worker completes")
+ tst.inFlightMu.RUnlock()
+}
+
+func Test_mergePartsWithConflictingTagTypes(t *testing.T) {
+ tests := []struct {
+ name string
+ parts []*traces
+ wantTagTypes map[string]pbv1.ValueType
+ wantDeleted []string
+ }{
+ {
+ name: "multiple parts with int and str for same tag",
+ parts: []*traces{
+ {
+ traceIDs: []string{"trace1"},
+ timestamps: []int64{1},
+ tags: [][]*tagValue{
+ {
+ {tag: "state", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(100)},
+ {tag: "name", valueType: pbv1.ValueTypeStr, value: []byte("svc1")},
+ },
+ },
+ spans: [][]byte{[]byte("span1")},
+ spanIDs: []string{"span1"},
+ },
+ {
+ traceIDs: []string{"trace2"},
+ timestamps: []int64{2},
+ tags: [][]*tagValue{
+ {
+ {tag: "state", valueType: pbv1.ValueTypeStr, value: []byte("active")},
+ {tag: "name", valueType: pbv1.ValueTypeStr, value: []byte("svc2")},
+ },
+ },
+ spans: [][]byte{[]byte("span2")},
+ spanIDs: []string{"span2"},
+ },
+ },
+ wantTagTypes: map[string]pbv1.ValueType{
+ "state#int": pbv1.ValueTypeInt64,
+ "state#str": pbv1.ValueTypeStr,
+ "name": pbv1.ValueTypeStr,
+ },
+ wantDeleted: []string{"state"},
+ },
+ {
+ name: "merge typed-suffix part with plain part",
+ parts: []*traces{
+ {
+ traceIDs: []string{"trace1"},
+ timestamps: []int64{1},
+ tags: [][]*tagValue{
+ {{tag: "state#int", valueType: pbv1.ValueTypeInt64, value: convert.Int64ToBytes(100)}},
+ },
+ spans: [][]byte{[]byte("span1")},
+ spanIDs: []string{"span1"},
+ },
+ {
+ traceIDs: []string{"trace2"},
+ timestamps: []int64{2},
+ tags: [][]*tagValue{
+ {{tag: "state", valueType: pbv1.ValueTypeStr, value: []byte("pending")}},
+ },
+ spans: [][]byte{[]byte("span2")},
+ spanIDs: []string{"span2"},
+ },
+ },
+ wantTagTypes: map[string]pbv1.ValueType{
+ "state#int": pbv1.ValueTypeInt64,
+ "state#str": pbv1.ValueTypeStr,
+ },
+ wantDeleted: []string{"state"},
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tmpPath, defFn := test.Space(require.New(t))
+ defer defFn()
+ fileSystem := fs.NewLocalFileSystem()
+
+ partIDs := make([]uint64, len(tt.parts))
+ for idx := range partIDs {
+ partIDs[idx] = uint64(idx)
+ }
+ dstID := uint64(999)
+ dstPath := mergePartsWithConflictTags(t, fileSystem, tmpPath, tt.parts, partIDs, dstID)
+
+ mergedPart := mustOpenFilePart(dstID, tmpPath, fileSystem)
+ for tag, wantType := range tt.wantTagTypes {
+ gotType, exists := mergedPart.tagType[tag]
+ require.True(t, exists, "expected tag type entry for %q", tag)
+ require.Equal(t, wantType, gotType, "tag type mismatch for %q", tag)
+ }
+ for _, tag := range tt.wantDeleted {
+ _, ok := mergedPart.tagType[tag]
+ require.False(t, ok, "tag %q should not exist in tagType after merge", tag)
+ dataFile := filepath.Join(dstPath, tag+tagsFilenameExt)
+ require.False(t, fileSystem.IsExist(dataFile), "tag data file %q should not exist", dataFile)
+ metaFile := filepath.Join(dstPath, tag+tagsMetadataFilenameExt)
+ require.False(t, fileSystem.IsExist(metaFile), "tag metadata file %q should not exist", metaFile)
+ }
+ })
+ }
+}
+
+func mergePartsWithConflictTags(t *testing.T, fileSystem fs.FileSystem, tmpPath string, tsList []*traces, partIDs []uint64, dstID uint64) string {
+ t.Helper()
+ var parts []*partWrapper
+ for i, traces := range tsList {
+ mp := generateMemPart()
+ mp.mustInitFromTraces(traces)
+ mp.mustFlush(fileSystem, partPath(tmpPath, partIDs[i]))
+ p := mustOpenFilePart(partIDs[i], tmpPath, fileSystem)
+ parts = append(parts, &partWrapper{p: p})
+ releaseMemPart(mp)
+ }
+
+ conflictTags := collectConflictTags(parts)
+
+ var pmi []*partMergeIter
+ var traceSize uint64
+ for i := range tsList {
+ p := mustOpenFilePart(partIDs[i], tmpPath, fileSystem)
+ iter := generatePartMergeIter()
+ iter.mustInitFromPart(p)
+ pmi = append(pmi, iter)
+ traceSize += uint64(len(tsList[i].traceIDs))
+ }
+
+ br := generateBlockReader()
+ br.init(pmi)
+ bw := generateBlockWriter()
+ dstPath := partPath(tmpPath, dstID)
+ bw.mustInitForFilePart(fileSystem, dstPath, false, int(traceSize))
+
+ closeCh := make(chan struct{})
+ pm, tf, tagTypes, mergeErr := mergeBlocks(closeCh, bw, br, conflictTags)
+ close(closeCh)
+ require.NoError(t, mergeErr)
+ require.NotNil(t, pm)
+
+ releaseBlockWriter(bw)
+ releaseBlockReader(br)
+ for _, iter := range pmi {
+ releasePartMergeIter(iter)
+ }
+
+ pm.mustWriteMetadata(fileSystem, dstPath)
+ tf.mustWriteTraceIDFilter(fileSystem, dstPath)
+ tagTypes.mustWriteTagType(fileSystem, dstPath)
+ fileSystem.SyncPath(dstPath)
+ return dstPath
+}
diff --git a/banyand/trace/syncer.go b/banyand/trace/syncer.go
index eb942b107..a03471cdc 100644
--- a/banyand/trace/syncer.go
+++ b/banyand/trace/syncer.go
@@ -475,11 +475,7 @@ func (tst *tsTable) handleSyncIntroductions(partsToSync []*part, syncCh chan *sy
return errClosed
}
- select {
- case <-si.applied:
- case <-tst.loopCloser.CloseNotify():
- return errClosed
- }
+ <-si.applied
return nil
}
diff --git a/banyand/trace/tstable.go b/banyand/trace/tstable.go
index 05d5e11d1..cad0dea0b 100644
--- a/banyand/trace/tstable.go
+++ b/banyand/trace/tstable.go
@@ -518,10 +518,7 @@ func (tst *tsTable) mustAddFilePart(partID uint64, sidxFilePartsMap map[string]s
ind.part.decRef()
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
}
func (tst *tsTable) mustAddMemPart(mp *memPart, sidxReqsMap map[string]*sidx.MemPart) {
@@ -543,10 +540,7 @@ func (tst *tsTable) mustAddMemPart(mp *memPart, sidxReqsMap map[string]*sidx.Mem
ind.part.decRef()
return
}
- select {
- case <-ind.applied:
- case <-tst.loopCloser.CloseNotify():
- }
+ <-ind.applied
tst.incTotalWritten(int(totalCount))
tst.incTotalBatch(1)
tst.incTotalBatchIntroLatency(time.Since(startTime).Seconds())
(skywalking-banyandb) 05/24: fix(storage): prevent epoch segment creation from zero timestamps (#1059)
Posted by ha...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
hanahmily pushed a commit to branch v0.10.x
in repository https://gitbox.apache.org/repos/asf/skywalking-banyandb.git
commit 256835ca648e6512b072614faa1772fc32927cf0
Author: Gao Hongtao <ha...@gmail.com>
AuthorDate: Fri Apr 10 10:27:42 2026 +0800
fix(storage): prevent epoch segment creation from zero timestamps (#1059)
* fix(storage): prevent epoch segment creation from zero timestamps
Zero-value MinTimestamp in distributed sync paths caused seg-19700101
directories to be created with valid metadata, persisting across restarts
and corrupting retention logic.
Defense-in-depth fix:
- Reject timestamps before year 2000 in segmentController.create()
- Clean up existing epoch segments on TSDB open
- Validate MinTimestamp > 0 at all sync entry points (trace/measure/stream)
- Guard Tick() against zero timestamps in rotation
---
banyand/internal/storage/rotation.go | 3 ++
banyand/internal/storage/rotation_test.go | 55 ++++++++++++++++++---
banyand/internal/storage/segment.go | 72 +++++++++++++++++++--------
banyand/internal/storage/tsdb_test.go | 81 +++++++++++++++++++++++--------
banyand/measure/write_data.go | 6 +++
banyand/measure/write_index.go | 5 ++
banyand/stream/write_data.go | 9 ++++
banyand/stream/write_index.go | 9 ++++
banyand/trace/write_data.go | 6 +++
banyand/trace/write_index.go | 5 ++
10 files changed, 202 insertions(+), 49 deletions(-)
diff --git a/banyand/internal/storage/rotation.go b/banyand/internal/storage/rotation.go
index fd30f3edd..3923cc99c 100644
--- a/banyand/internal/storage/rotation.go
+++ b/banyand/internal/storage/rotation.go
@@ -32,6 +32,9 @@ var (
)
func (d *database[T, O]) Tick(ts int64) {
+ if ts <= 0 {
+ return
+ }
if (ts - timeEventSnapDuration) < d.latestTickTime.Load() {
return
}
diff --git a/banyand/internal/storage/rotation_test.go b/banyand/internal/storage/rotation_test.go
index b9251a357..9577c6736 100644
--- a/banyand/internal/storage/rotation_test.go
+++ b/banyand/internal/storage/rotation_test.go
@@ -208,25 +208,20 @@ var MockTSTableCreator = func(_ fs.FileSystem, _ string, _ common.Position,
return &MockTSTable{}, nil
}
-type SnapshotMockTSTable struct {
- timeRange timestamp.TimeRange
-}
+type SnapshotMockTSTable struct{}
func (m *SnapshotMockTSTable) Close() error { return nil }
func (m *SnapshotMockTSTable) Collect(_ Metrics) {}
func (m *SnapshotMockTSTable) TakeFileSnapshot(_ string) (bool, error) {
- if m.timeRange.Start.Equal(time.Unix(0, 0)) {
- return false, ErrNoCurrentSnapshot
- }
return true, nil
}
var SnapshotMockTSTableCreator = func(_ fs.FileSystem, _ string, _ common.Position,
- _ *logger.Logger, timeRange timestamp.TimeRange, _, _ any,
+ _ *logger.Logger, _ timestamp.TimeRange, _, _ any,
) (*SnapshotMockTSTable, error) {
- return &SnapshotMockTSTable{timeRange: timeRange}, nil
+ return &SnapshotMockTSTable{}, nil
}
type MockMetrics struct{}
@@ -307,3 +302,47 @@ func TestRotationDisabled(t *testing.T) {
assert.Equal(t, "2024-05-07 00:00:00", segments[1].End.Format("2006-01-02 15:04:05"))
})
}
+
+func TestTickRejectsZeroTimestamp(t *testing.T) {
+ logger.Init(logger.Logging{
+ Env: "dev",
+ Level: flags.LogLevel,
+ })
+
+ dir, defFn := test.Space(require.New(t))
+ defer defFn()
+
+ opts := TSDBOpts[*MockTSTable, any]{
+ Location: dir,
+ SegmentInterval: IntervalRule{Unit: DAY, Num: 1},
+ TTL: IntervalRule{Unit: DAY, Num: 7},
+ ShardNum: 1,
+ TSTableCreator: MockTSTableCreator,
+ }
+
+ ctx := context.Background()
+ mc := timestamp.NewMockClock()
+ ts, err := time.ParseInLocation("2006-01-02 15:04:05", "2024-05-01 00:00:00", time.Local)
+ require.NoError(t, err)
+ mc.Set(ts)
+ ctx = timestamp.SetClock(ctx, mc)
+
+ tsdb, err := OpenTSDB(ctx, opts, NewServiceCache(), group)
+ require.NoError(t, err)
+ defer tsdb.Close()
+
+ db := tsdb.(*database[*MockTSTable, any])
+
+ // Tick with a valid timestamp to set latestTickTime
+ validTS := ts.UnixNano()
+ tsdb.Tick(validTS)
+ require.Equal(t, validTS, db.latestTickTime.Load(), "valid tick should update latestTickTime")
+
+ // Tick with zero should be rejected and not update latestTickTime
+ tsdb.Tick(0)
+ require.Equal(t, validTS, db.latestTickTime.Load(), "Tick(0) should not update latestTickTime")
+
+ // Tick with negative should also be rejected
+ tsdb.Tick(-1)
+ require.Equal(t, validTS, db.latestTickTime.Load(), "Tick(-1) should not update latestTickTime")
+}
diff --git a/banyand/internal/storage/segment.go b/banyand/internal/storage/segment.go
index f9ad6a122..04a333507 100644
--- a/banyand/internal/storage/segment.go
+++ b/banyand/internal/storage/segment.go
@@ -19,6 +19,7 @@ package storage
import (
"context"
+ "encoding/json"
"fmt"
"io/fs"
"path"
@@ -33,15 +34,19 @@ import (
"github.com/apache/skywalking-banyandb/api/common"
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
- "github.com/apache/skywalking-banyandb/pkg/convert"
banyanfs "github.com/apache/skywalking-banyandb/pkg/fs"
"github.com/apache/skywalking-banyandb/pkg/index/inverted"
"github.com/apache/skywalking-banyandb/pkg/logger"
"github.com/apache/skywalking-banyandb/pkg/timestamp"
)
-// ErrSegmentClosed is returned when trying to access a closed segment.
-var ErrSegmentClosed = errors.New("segment closed")
+var (
+ // ErrSegmentClosed is returned when trying to access a closed segment.
+ ErrSegmentClosed = errors.New("segment closed")
+ // ErrInvalidSegmentTimestamp is returned when a segment timestamp is zero or near-epoch,
+ // indicating a corrupted timestamp (e.g., unset MinTimestamp flowing through sync paths).
+ ErrInvalidSegmentTimestamp = errors.New("invalid segment timestamp: epoch or near-epoch time is not valid for APM data")
+)
var _ Cache = (*segmentCache)(nil)
@@ -477,39 +482,59 @@ func (sc *segmentController[T, O]) parse(value string) (time.Time, error) {
func (sc *segmentController[T, O]) open() error {
sc.Lock()
defer sc.Unlock()
- emptySegments := make([]string, 0)
+ invalidSegments := make([]string, 0)
err := loadSegments(sc.location, segPathPrefix, sc, sc.getOptions().SegmentInterval, func(start, end time.Time) error {
suffix := sc.format(start)
segmentPath := path.Join(sc.location, fmt.Sprintf(segTemplate, suffix))
+ // Detect epoch/near-epoch segments created by zero MinTimestamp in sync paths.
+ // BanyanDB is an APM database -- no legitimate data predates the year 2000.
+ if start.UnixNano() <= 0 {
+ invalidSegments = append(invalidSegments, segmentPath)
+ return nil
+ }
metadataPath := path.Join(segmentPath, metadataFilename)
- version, err := sc.lfs.Read(metadataPath)
- if err != nil {
- if errors.Is(err, fs.ErrNotExist) {
- emptySegments = append(emptySegments, segmentPath)
+ rawMeta, readErr := sc.lfs.Read(metadataPath)
+ if readErr != nil {
+ if errors.Is(readErr, fs.ErrNotExist) {
+ invalidSegments = append(invalidSegments, segmentPath)
return nil
}
- return err
+ return readErr
}
- if len(version) == 0 {
- emptySegments = append(emptySegments, segmentPath)
+ if len(rawMeta) == 0 {
+ invalidSegments = append(invalidSegments, segmentPath)
return nil
}
- if err = checkVersion(convert.BytesToString(version)); err != nil {
- return err
+ meta, parseErr := readSegmentMeta(rawMeta)
+ if parseErr != nil {
+ return parseErr
}
- _, err = sc.load(start, end, sc.location)
- return err
+ segmentEnd := end
+ if meta.EndTime != "" {
+ parsedEnd, timeErr := time.Parse(time.RFC3339Nano, meta.EndTime)
+ if timeErr != nil {
+ return timeErr
+ }
+ segmentEnd = parsedEnd
+ }
+ _, loadErr := sc.load(start, segmentEnd, sc.location)
+ return loadErr
})
- if len(emptySegments) > 0 {
- sc.l.Warn().Strs("segments", emptySegments).Msg("empty segments found, removing them.")
- for i := range emptySegments {
- sc.lfs.MustRMAll(emptySegments[i])
+ if len(invalidSegments) > 0 {
+ sc.l.Warn().Strs("segments", invalidSegments).Msg("invalid segments found (empty or epoch-dated), removing them")
+ for i := range invalidSegments {
+ sc.lfs.MustRMAll(invalidSegments[i])
}
}
return err
}
func (sc *segmentController[T, O]) create(start time.Time) (*segment[T, O], error) {
+ // Reject epoch/near-epoch timestamps caused by zero MinTimestamp flowing through sync paths.
+ // BanyanDB is an APM database -- no legitimate data predates the year 2000.
+ if start.UnixNano() <= 0 {
+ return nil, ErrInvalidSegmentTimestamp
+ }
sc.Lock()
defer sc.Unlock()
last := len(sc.lst) - 1
@@ -539,7 +564,14 @@ func (sc *segmentController[T, O]) create(start time.Time) (*segment[T, O], erro
}
segPath := path.Join(sc.location, fmt.Sprintf(segTemplate, sc.format(start)))
sc.lfs.MkdirPanicIfExist(segPath, DirPerm)
- data := []byte(currentVersion)
+ meta := segmentMeta{
+ Version: currentVersion,
+ EndTime: end.Format(time.RFC3339Nano),
+ }
+ data, marshalErr := json.Marshal(meta)
+ if marshalErr != nil {
+ logger.Panicf("cannot marshal segment metadata: %s", marshalErr)
+ }
metadataPath := filepath.Join(segPath, metadataFilename)
lf, err := sc.lfs.CreateLockFile(metadataPath, FilePerm)
if err != nil {
diff --git a/banyand/internal/storage/tsdb_test.go b/banyand/internal/storage/tsdb_test.go
index 08b285d7a..cf87cb3f5 100644
--- a/banyand/internal/storage/tsdb_test.go
+++ b/banyand/internal/storage/tsdb_test.go
@@ -27,6 +27,7 @@ import (
"go.uber.org/mock/gomock"
commonv1 "github.com/apache/skywalking-banyandb/api/proto/banyandb/common/v1"
+ "github.com/apache/skywalking-banyandb/pkg/fs"
"github.com/apache/skywalking-banyandb/pkg/logger"
"github.com/apache/skywalking-banyandb/pkg/meter"
"github.com/apache/skywalking-banyandb/pkg/test"
@@ -309,12 +310,10 @@ func TestTakeFileSnapshot(t *testing.T) {
require.NoError(t, tsdb.Close())
})
- t.Run("Take snapshot skips shard with no current snapshot", func(t *testing.T) {
+ t.Run("Epoch timestamp segment creation is rejected", func(t *testing.T) {
dir, defFn := test.Space(require.New(t))
defer defFn()
- snapshotDir := filepath.Join(dir, "snapshot")
-
opts := TSDBOpts[*SnapshotMockTSTable, any]{
Location: dir,
SegmentInterval: IntervalRule{Unit: DAY, Num: 1},
@@ -335,29 +334,69 @@ func TestTakeFileSnapshot(t *testing.T) {
require.NoError(t, err)
defer tsdb.Close()
- normalSeg, err := tsdb.CreateSegmentIfNotExist(ts)
- require.NoError(t, err)
- normalSegLocation := normalSeg.(*segment[*SnapshotMockTSTable, any]).location
+ normalSeg, segErr := tsdb.CreateSegmentIfNotExist(ts)
+ require.NoError(t, segErr)
normalSeg.DecRef()
- epochSeg, err := tsdb.CreateSegmentIfNotExist(time.Unix(0, 0))
- require.NoError(t, err)
- epochSegLocation := epochSeg.(*segment[*SnapshotMockTSTable, any]).location
- epochSeg.DecRef()
+ _, epochErr := tsdb.CreateSegmentIfNotExist(time.Unix(0, 0))
+ require.ErrorIs(t, epochErr, ErrInvalidSegmentTimestamp,
+ "creating a segment with epoch timestamp should be rejected")
+ })
+}
- created, snapshotErr := tsdb.TakeFileSnapshot(snapshotDir)
- require.NoError(t, snapshotErr,
- "snapshot should not fail due to empty shard in epoch segment")
- require.True(t, created)
+func TestEpochSegmentCleanupOnOpen(t *testing.T) {
+ logger.Init(logger.Logging{
+ Env: "dev",
+ Level: flags.LogLevel,
+ })
- normalSegDir := filepath.Join(snapshotDir, filepath.Base(normalSegLocation))
- require.DirExists(t, normalSegDir,
- "normal segment should be present in snapshot")
+ dir, defFn := test.Space(require.New(t))
+ defer defFn()
- epochSegDir := filepath.Join(snapshotDir, filepath.Base(epochSegLocation))
- require.DirExists(t, epochSegDir,
- "epoch segment directory should still be created")
- })
+ opts := TSDBOpts[*MockTSTable, any]{
+ Location: dir,
+ SegmentInterval: IntervalRule{Unit: DAY, Num: 1},
+ TTL: IntervalRule{Unit: DAY, Num: 7},
+ ShardNum: 1,
+ TSTableCreator: MockTSTableCreator,
+ }
+
+ ctx := context.Background()
+ mc := timestamp.NewMockClock()
+ ts, err := time.ParseInLocation("2006-01-02 15:04:05", "2024-05-01 00:00:00", time.Local)
+ require.NoError(t, err)
+ mc.Set(ts)
+ ctx = timestamp.SetClock(ctx, mc)
+
+ // Create a TSDB with a valid segment
+ sc := NewServiceCache()
+ tsdb1, err := OpenTSDB(ctx, opts, sc, group)
+ require.NoError(t, err)
+
+ seg, segErr := tsdb1.CreateSegmentIfNotExist(ts)
+ require.NoError(t, segErr)
+ seg.DecRef()
+ require.NoError(t, tsdb1.Close())
+
+ // Manually create a fake epoch segment directory with metadata to simulate the bug
+ epochSegDir := filepath.Join(dir, "seg-19700101")
+ lfs := fs.NewLocalFileSystem()
+ lfs.MkdirIfNotExist(epochSegDir, DirPerm)
+ metaPath := filepath.Join(epochSegDir, metadataFilename)
+ lf, lockErr := lfs.CreateLockFile(metaPath, FilePerm)
+ require.NoError(t, lockErr)
+ _, writeErr := lf.Write([]byte(`{"version":"v1","end_time":"1970-01-02T00:00:00Z"}`))
+ require.NoError(t, writeErr)
+ require.NoError(t, lf.Close())
+
+ require.DirExists(t, epochSegDir, "epoch segment should exist before reopening")
+
+ // Reopen TSDB - it should clean up the epoch segment
+ tsdb2, err := OpenTSDB(ctx, opts, sc, group)
+ require.NoError(t, err)
+ defer tsdb2.Close()
+
+ require.NoDirExists(t, epochSegDir, "epoch segment should be removed on open")
}
func TestTSDBCollect(t *testing.T) {
diff --git a/banyand/measure/write_data.go b/banyand/measure/write_data.go
index ef4fe160a..cc5b855b7 100644
--- a/banyand/measure/write_data.go
+++ b/banyand/measure/write_data.go
@@ -81,6 +81,9 @@ func (s *syncCallback) CheckHealth() *common.Error {
// CreatePartHandler implements queue.ChunkedSyncHandler.
func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (queue.PartHandler, error) {
+ if ctx.MinTimestamp <= 0 {
+ return nil, fmt.Errorf("invalid MinTimestamp %d in chunk sync context for group %s", ctx.MinTimestamp, ctx.Group)
+ }
tsdb, err := s.schemaRepo.loadTSDB(ctx.Group)
if err != nil {
s.l.Error().Err(err).Str("group", ctx.Group).Msg("failed to load TSDB for group")
@@ -236,6 +239,9 @@ func (s *syncSeriesCallback) CheckHealth() *common.Error {
// CreatePartHandler implements queue.ChunkedSyncHandler for series index synchronization.
func (s *syncSeriesCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (queue.PartHandler, error) {
+ if ctx.MinTimestamp <= 0 {
+ return nil, fmt.Errorf("invalid MinTimestamp %d in series sync context for group %s", ctx.MinTimestamp, ctx.Group)
+ }
tsdb, err := s.schemaRepo.loadTSDB(ctx.Group)
if err != nil {
s.l.Error().Err(err).Str("group", ctx.Group).Msg("failed to load TSDB for group")
diff --git a/banyand/measure/write_index.go b/banyand/measure/write_index.go
index b7146074d..7babb7ba9 100644
--- a/banyand/measure/write_index.go
+++ b/banyand/measure/write_index.go
@@ -82,6 +82,11 @@ func (i *indexCallback) Rev(_ context.Context, message bus.Message) (resp bus.Me
return
}
+ if timestamp <= 0 {
+ i.l.Error().Int64("timestamp", timestamp).Str("group", group).Msg("invalid timestamp in index insert message, skipping")
+ return
+ }
+
// Get TSDB by group name using schemaRepo
tsdb, err := i.schemaRepo.loadTSDB(group)
if err != nil {
diff --git a/banyand/stream/write_data.go b/banyand/stream/write_data.go
index 93c5dfe08..1eeaadded 100644
--- a/banyand/stream/write_data.go
+++ b/banyand/stream/write_data.go
@@ -80,6 +80,9 @@ func (s *syncCallback) CheckHealth() *common.Error {
// CreatePartHandler implements queue.ChunkedSyncHandler.
func (s *syncCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (queue.PartHandler, error) {
+ if ctx.MinTimestamp <= 0 {
+ return nil, fmt.Errorf("invalid MinTimestamp %d in chunk sync context for group %s", ctx.MinTimestamp, ctx.Group)
+ }
tsdb, err := s.schemaRepo.loadTSDB(ctx.Group)
if err != nil {
s.l.Error().Err(err).Str("group", ctx.Group).Msg("failed to load TSDB for group")
@@ -201,6 +204,9 @@ func (s *syncSeriesCallback) CheckHealth() *common.Error {
// CreatePartHandler implements queue.ChunkedSyncHandler for series index synchronization.
func (s *syncSeriesCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (queue.PartHandler, error) {
+ if ctx.MinTimestamp <= 0 {
+ return nil, fmt.Errorf("invalid MinTimestamp %d in series sync context for group %s", ctx.MinTimestamp, ctx.Group)
+ }
tsdb, err := s.schemaRepo.loadTSDB(ctx.Group)
if err != nil {
s.l.Error().Err(err).Str("group", ctx.Group).Msg("failed to load TSDB for group")
@@ -306,6 +312,9 @@ func (s *syncElementIndexCallback) CheckHealth() *common.Error {
// CreatePartHandler implements queue.ChunkedSyncHandler for element index synchronization.
func (s *syncElementIndexCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (queue.PartHandler, error) {
+ if ctx.MinTimestamp <= 0 {
+ return nil, fmt.Errorf("invalid MinTimestamp %d in element index sync context for group %s", ctx.MinTimestamp, ctx.Group)
+ }
tsdb, err := s.schemaRepo.loadTSDB(ctx.Group)
if err != nil {
s.l.Error().Err(err).Str("group", ctx.Group).Msg("failed to load TSDB for group")
diff --git a/banyand/stream/write_index.go b/banyand/stream/write_index.go
index 0d4e3200a..e39dfd6a6 100644
--- a/banyand/stream/write_index.go
+++ b/banyand/stream/write_index.go
@@ -79,6 +79,11 @@ func (s *seriesIndexCallback) Rev(_ context.Context, message bus.Message) (resp
return
}
+ if timestamp <= 0 {
+ s.l.Error().Int64("timestamp", timestamp).Str("group", group).Msg("invalid timestamp in series index message, skipping")
+ return
+ }
+
// Get TSDB by group name using schemaRepo
tsdb, err := s.schemaRepo.loadTSDB(group)
if err != nil {
@@ -170,6 +175,10 @@ func (l *localIndexCallback) Rev(_ context.Context, message bus.Message) (resp b
}
// Use the first document's timestamp to get the segment
+ if documents[0].Timestamp <= 0 {
+ l.l.Error().Int64("timestamp", documents[0].Timestamp).Str("group", group).Msg("invalid document timestamp in local index message, skipping")
+ return
+ }
firstDocTime := time.Unix(0, documents[0].Timestamp)
segment, err := tsdb.CreateSegmentIfNotExist(firstDocTime)
if err != nil {
diff --git a/banyand/trace/write_data.go b/banyand/trace/write_data.go
index 63c56c2df..c7290c349 100644
--- a/banyand/trace/write_data.go
+++ b/banyand/trace/write_data.go
@@ -171,6 +171,9 @@ func (s *syncSeriesCallback) CheckHealth() *common.Error {
}
func (s *syncSeriesCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (queue.PartHandler, error) {
+ if ctx.MinTimestamp <= 0 {
+ return nil, fmt.Errorf("invalid MinTimestamp %d in series sync context for group %s", ctx.MinTimestamp, ctx.Group)
+ }
tsdb, err := s.schemaRepo.loadTSDB(ctx.Group)
if err != nil {
s.l.Error().Err(err).Str("group", ctx.Group).Msg("failed to load TSDB for group")
@@ -242,6 +245,9 @@ func (s *syncChunkCallback) CheckHealth() *common.Error {
// CreatePartHandler implements queue.ChunkedSyncHandler.
func (s *syncChunkCallback) CreatePartHandler(ctx *queue.ChunkedSyncPartContext) (queue.PartHandler, error) {
+ if ctx.MinTimestamp <= 0 {
+ return nil, fmt.Errorf("invalid MinTimestamp %d in chunk sync context for group %s", ctx.MinTimestamp, ctx.Group)
+ }
tsdb, err := s.schemaRepo.loadTSDB(ctx.Group)
if err != nil {
s.l.Error().Err(err).Str("group", ctx.Group).Msg("failed to load TSDB for group")
diff --git a/banyand/trace/write_index.go b/banyand/trace/write_index.go
index c5235ed23..c032f79b3 100644
--- a/banyand/trace/write_index.go
+++ b/banyand/trace/write_index.go
@@ -79,6 +79,11 @@ func (s *seriesIndexCallback) Rev(_ context.Context, message bus.Message) (resp
return
}
+ if timestamp <= 0 {
+ s.l.Error().Int64("timestamp", timestamp).Str("group", group).Msg("invalid timestamp in series index message, skipping")
+ return
+ }
+
// Get TSDB by group name using schemaRepo
tsdb, err := s.schemaRepo.loadTSDB(group)
if err != nil {