From fe7b7ce2f9902fffaa5963173d9534f28fd49a2d Mon Sep 17 00:00:00 2001
From: ucwong
Date: Thu, 25 Dec 2025 23:38:44 +0800
Subject: [PATCH] deps
---
go.mod | 25 +-
go.sum | 51 +-
.../google/flatbuffers/go/encode.go | 2 +-
.../jedib0t/go-pretty/v6/progress/README.md | 10 +-
.../go-pretty/v6/progress/indicator.go | 5 +-
.../jedib0t/go-pretty/v6/progress/progress.go | 8 +-
.../jedib0t/go-pretty/v6/progress/render.go | 241 +++-
.../jedib0t/go-pretty/v6/progress/tracker.go | 31 +-
.../go-pretty/v6/progress/tracker_sort.go | 92 +-
.../jedib0t/go-pretty/v6/text/ansi.go | 20 +-
.../jedib0t/go-pretty/v6/text/valign.go | 7 +-
.../jedib0t/go-pretty/v6/text/wrap.go | 6 +-
vendor/github.com/nutsdb/nutsdb/.gitignore | 8 +-
vendor/github.com/nutsdb/nutsdb/.travis.yml | 10 -
vendor/github.com/nutsdb/nutsdb/README-CN.md | 5 +-
vendor/github.com/nutsdb/nutsdb/README.md | 226 ++-
vendor/github.com/nutsdb/nutsdb/batch.go | 2 +-
vendor/github.com/nutsdb/nutsdb/bucket.go | 4 +-
.../nutsdb/nutsdb/bucket_manager.go | 10 +-
vendor/github.com/nutsdb/nutsdb/datafile.go | 6 +-
vendor/github.com/nutsdb/nutsdb/db.go | 401 +++++-
vendor/github.com/nutsdb/nutsdb/db_error.go | 6 +
.../github.com/nutsdb/nutsdb/entity_utils.go | 28 -
vendor/github.com/nutsdb/nutsdb/entry.go | 60 +-
vendor/github.com/nutsdb/nutsdb/errors.go | 8 +-
.../github.com/nutsdb/nutsdb/file_manager.go | 84 +-
.../nutsdb/nutsdb/hint_collector.go | 100 ++
vendor/github.com/nutsdb/nutsdb/hintfile.go | 472 ++++++
vendor/github.com/nutsdb/nutsdb/index.go | 41 +-
.../nutsdb/{ => internal/data}/btree.go | 66 +-
.../internal/data/doubly_linked_list.go | 298 ++++
.../nutsdb/nutsdb/internal/data/item.go | 13 +
.../nutsdb/nutsdb/internal/data/list.go | 551 +++++++
.../nutsdb/{ => internal/data}/record.go | 29 +-
.../nutsdb/nutsdb/{ => internal/data}/set.go | 17 +-
.../nutsdb/nutsdb/internal/fileio/const.go | 17 +
.../{ => internal/fileio}/fd_manager.go | 57 +-
.../nutsdb/internal/fileio/fileutils.go | 21 +
.../nutsdb/{ => internal/fileio}/rwmanager.go | 13 +-
.../{ => internal/fileio}/rwmanger_fileio.go | 22 +-
.../nutsdb/internal/fileio/rwmanger_mmap.go | 182 +++
.../{ => internal/testutils}/test_utils.go | 26 +-
.../nutsdb/nutsdb/{ => internal/utils}/lru.go | 2 +-
.../nutsdb/nutsdb/{ => internal/utils}/tar.go | 43 +-
.../nutsdb/nutsdb/internal/utils/utils.go | 132 ++
vendor/github.com/nutsdb/nutsdb/iterator.go | 85 +-
vendor/github.com/nutsdb/nutsdb/list.go | 400 ------
vendor/github.com/nutsdb/nutsdb/merge.go | 204 ++-
.../nutsdb/nutsdb/merge_manifest.go | 65 +
.../nutsdb/nutsdb/merge_recovery.go | 50 +
.../github.com/nutsdb/nutsdb/merge_utils.go | 111 ++
vendor/github.com/nutsdb/nutsdb/merge_v2.go | 726 ++++++++++
vendor/github.com/nutsdb/nutsdb/metadata.go | 31 +-
vendor/github.com/nutsdb/nutsdb/options.go | 121 +-
vendor/github.com/nutsdb/nutsdb/pending.go | 119 +-
.../github.com/nutsdb/nutsdb/rwmanger_mmap.go | 81 --
vendor/github.com/nutsdb/nutsdb/sorted_set.go | 55 +-
vendor/github.com/nutsdb/nutsdb/tx.go | 171 ++-
vendor/github.com/nutsdb/nutsdb/tx_btree.go | 455 ++++--
vendor/github.com/nutsdb/nutsdb/tx_bucket.go | 10 +-
vendor/github.com/nutsdb/nutsdb/tx_error.go | 13 +-
vendor/github.com/nutsdb/nutsdb/tx_list.go | 89 +-
vendor/github.com/nutsdb/nutsdb/tx_set.go | 27 +-
vendor/github.com/nutsdb/nutsdb/tx_zset.go | 3 +-
vendor/github.com/nutsdb/nutsdb/utils.go | 183 +--
.../github.com/nutsdb/nutsdb/watch_manager.go | 585 ++++++++
.../pion/rtp/abscapturetimeextension.go | 34 +-
.../pion/rtp/abssendtimeextension.go | 19 +
.../pion/rtp/audiolevelextension.go | 24 +
.../pion/rtp/playoutdelayextension.go | 22 +
.../pion/rtp/transportccextension.go | 17 +
vendor/github.com/pion/rtp/vlaextension.go | 146 +-
vendor/github.com/pion/sctp/association.go | 1280 +++++++++++++++--
.../github.com/pion/sctp/chunk_heartbeat.go | 89 +-
.../pion/sctp/chunk_heartbeat_ack.go | 53 +-
.../github.com/pion/sctp/chunk_init_common.go | 11 +
.../pion/sctp/chunk_payload_data.go | 4 +
vendor/github.com/pion/sctp/packet.go | 36 +-
vendor/github.com/pion/sctp/windowedmin.go | 81 ++
vendor/github.com/pion/sdp/v3/base_lexer.go | 9 +-
vendor/github.com/pion/sdp/v3/jsep.go | 26 +-
vendor/github.com/pion/sdp/v3/util.go | 21 +-
.../pion/webrtc/v4/dtlstransport_js.go | 29 +
vendor/github.com/pion/webrtc/v4/errors.go | 1 +
.../pion/webrtc/v4/icecandidatetype.go | 6 +-
.../github.com/pion/webrtc/v4/icegatherer.go | 326 ++++-
.../pion/webrtc/v4/icetransportpolicy.go | 12 +-
.../pion/webrtc/v4/internal/mux/endpoint.go | 23 +-
.../github.com/pion/webrtc/v4/networktype.go | 19 +-
.../pion/webrtc/v4/peerconnection.go | 9 +-
.../github.com/pion/webrtc/v4/rtpreceiver.go | 38 +-
.../pion/webrtc/v4/sctptransport.go | 11 +
vendor/github.com/pion/webrtc/v4/sdp.go | 54 +-
.../pion/webrtc/v4/settingengine.go | 99 +-
.../pion/webrtc/v4/track_local_static.go | 26 +-
vendor/github.com/xujiajun/mmap-go/.gitignore | 10 -
.../github.com/xujiajun/mmap-go/.travis.yml | 16 -
vendor/github.com/xujiajun/mmap-go/LICENSE | 25 -
vendor/github.com/xujiajun/mmap-go/README.md | 12 -
vendor/github.com/xujiajun/mmap-go/mmap.go | 117 --
.../github.com/xujiajun/mmap-go/mmap_unix.go | 51 -
.../xujiajun/mmap-go/mmap_windows.go | 153 --
.../{COPYRIGHT-MUSL => LICENSE-3RD-PARTY.md} | 112 ++
vendor/modernc.org/libc/LICENSE-GO | 27 -
.../libc/honnef.co/go/netdb/LICENSE | 20 -
.../libc/honnef.co/go/netdb/netdb.go | 6 +-
vendor/modernc.org/sqlite/AUTHORS | 2 +-
vendor/modernc.org/sqlite/CONTRIBUTORS | 1 +
.../sqlite/lib/sqlite_darwin_amd64.go | 1 -
.../sqlite/lib/sqlite_darwin_arm64.go | 1 -
.../sqlite/lib/sqlite_freebsd_amd64.go | 1 -
.../sqlite/lib/sqlite_freebsd_arm64.go | 1 -
.../sqlite/lib/sqlite_linux_386.go | 1 -
.../sqlite/lib/sqlite_linux_amd64.go | 1 -
.../sqlite/lib/sqlite_linux_arm.go | 1 -
.../sqlite/lib/sqlite_linux_arm64.go | 1 -
.../sqlite/lib/sqlite_linux_loong64.go | 1 -
.../sqlite/lib/sqlite_linux_ppc64le.go | 1 -
.../sqlite/lib/sqlite_linux_riscv64.go | 1 -
.../sqlite/lib/sqlite_linux_s390x.go | 1 -
.../modernc.org/sqlite/lib/sqlite_windows.go | 1 -
.../sqlite/lib/sqlite_windows_386.go | 1 -
vendor/modules.txt | 33 +-
123 files changed, 7960 insertions(+), 2206 deletions(-)
delete mode 100644 vendor/github.com/nutsdb/nutsdb/.travis.yml
delete mode 100644 vendor/github.com/nutsdb/nutsdb/entity_utils.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/hint_collector.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/hintfile.go
rename vendor/github.com/nutsdb/nutsdb/{ => internal/data}/btree.go (60%)
create mode 100644 vendor/github.com/nutsdb/nutsdb/internal/data/doubly_linked_list.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/internal/data/item.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/internal/data/list.go
rename vendor/github.com/nutsdb/nutsdb/{ => internal/data}/record.go (77%)
rename vendor/github.com/nutsdb/nutsdb/{ => internal/data}/set.go (95%)
create mode 100644 vendor/github.com/nutsdb/nutsdb/internal/fileio/const.go
rename vendor/github.com/nutsdb/nutsdb/{ => internal/fileio}/fd_manager.go (77%)
create mode 100644 vendor/github.com/nutsdb/nutsdb/internal/fileio/fileutils.go
rename vendor/github.com/nutsdb/nutsdb/{ => internal/fileio}/rwmanager.go (78%)
rename vendor/github.com/nutsdb/nutsdb/{ => internal/fileio}/rwmanger_fileio.go (86%)
create mode 100644 vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanger_mmap.go
rename vendor/github.com/nutsdb/nutsdb/{ => internal/testutils}/test_utils.go (61%)
rename vendor/github.com/nutsdb/nutsdb/{ => internal/utils}/lru.go (99%)
rename vendor/github.com/nutsdb/nutsdb/{ => internal/utils}/tar.go (67%)
create mode 100644 vendor/github.com/nutsdb/nutsdb/internal/utils/utils.go
delete mode 100644 vendor/github.com/nutsdb/nutsdb/list.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/merge_manifest.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/merge_recovery.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/merge_utils.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/merge_v2.go
delete mode 100644 vendor/github.com/nutsdb/nutsdb/rwmanger_mmap.go
create mode 100644 vendor/github.com/nutsdb/nutsdb/watch_manager.go
create mode 100644 vendor/github.com/pion/sctp/windowedmin.go
delete mode 100644 vendor/github.com/xujiajun/mmap-go/.gitignore
delete mode 100644 vendor/github.com/xujiajun/mmap-go/.travis.yml
delete mode 100644 vendor/github.com/xujiajun/mmap-go/LICENSE
delete mode 100644 vendor/github.com/xujiajun/mmap-go/README.md
delete mode 100644 vendor/github.com/xujiajun/mmap-go/mmap.go
delete mode 100644 vendor/github.com/xujiajun/mmap-go/mmap_unix.go
delete mode 100644 vendor/github.com/xujiajun/mmap-go/mmap_windows.go
rename vendor/modernc.org/libc/{COPYRIGHT-MUSL => LICENSE-3RD-PARTY.md} (59%)
delete mode 100644 vendor/modernc.org/libc/LICENSE-GO
delete mode 100644 vendor/modernc.org/libc/honnef.co/go/netdb/LICENSE
diff --git a/go.mod b/go.mod
index e0a59debfa..b672a2f4bb 100644
--- a/go.mod
+++ b/go.mod
@@ -6,9 +6,9 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3
github.com/CortexFoundation/inference v1.0.2-0.20230307032835-9197d586a4e8
github.com/CortexFoundation/statik v0.0.0-20210315012922-8bb8a7b5dc66
- github.com/CortexFoundation/torrentfs v1.0.73-0.20251217130652-29bcb4ed05d5
+ github.com/CortexFoundation/torrentfs v1.0.73-0.20251221124821-bba7040b393f
github.com/Microsoft/go-winio v0.6.2
- github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62
+ github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549
github.com/VictoriaMetrics/fastcache v1.13.2
github.com/arsham/figurine v1.3.0
github.com/aws/aws-sdk-go-v2 v1.41.0
@@ -165,14 +165,14 @@ require (
github.com/goccy/go-json v0.10.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
- github.com/google/flatbuffers v25.9.23+incompatible // indirect
+ github.com/google/flatbuffers v25.12.19+incompatible // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
- github.com/jedib0t/go-pretty/v6 v6.7.7 // indirect
+ github.com/jedib0t/go-pretty/v6 v6.7.8 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
@@ -190,7 +190,7 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
- github.com/nutsdb/nutsdb v1.0.4 // indirect
+ github.com/nutsdb/nutsdb v1.1.0 // indirect
github.com/oapi-codegen/runtime v1.1.2 // indirect
github.com/otiai10/copy v1.14.1 // indirect
github.com/otiai10/mint v1.6.3 // indirect
@@ -203,15 +203,15 @@ require (
github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.16 // indirect
- github.com/pion/rtp v1.8.26 // indirect
- github.com/pion/sctp v1.8.41 // indirect
- github.com/pion/sdp/v3 v3.0.16 // indirect
+ github.com/pion/rtp v1.8.27 // indirect
+ github.com/pion/sctp v1.9.0 // indirect
+ github.com/pion/sdp/v3 v3.0.17 // indirect
github.com/pion/srtp/v3 v3.0.9 // indirect
github.com/pion/stun/v3 v3.0.2 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pion/turn/v4 v4.1.3 // indirect
- github.com/pion/webrtc/v4 v4.1.8 // indirect
+ github.com/pion/webrtc/v4 v4.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
@@ -239,7 +239,6 @@ require (
github.com/wlynxg/anet v0.0.5 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
- github.com/xujiajun/mmap-go v1.0.1 // indirect
github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
github.com/zeebo/xxh3 v1.0.3-0.20230502181907-3808c706a06a // indirect
@@ -249,16 +248,16 @@ require (
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 // indirect
+ golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/term v0.38.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.4.1 // indirect
- modernc.org/libc v1.67.1 // indirect
+ modernc.org/libc v1.67.2 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
- modernc.org/sqlite v1.40.1 // indirect
+ modernc.org/sqlite v1.41.0 // indirect
zombiezen.com/go/sqlite v1.4.2 // indirect
)
diff --git a/go.sum b/go.sum
index 4ac911cdda..e7220bb7e7 100644
--- a/go.sum
+++ b/go.sum
@@ -70,8 +70,8 @@ github.com/CortexFoundation/statik v0.0.0-20210315012922-8bb8a7b5dc66/go.mod h1:
github.com/CortexFoundation/torrentfs v1.0.13-0.20200623060705-ce027f43f2f8/go.mod h1:Ma+tGhPPvz4CEZHaqEJQMOEGOfHeQBiAoNd1zyc/w3Q=
github.com/CortexFoundation/torrentfs v1.0.14-0.20200703071639-3fcabcabf274/go.mod h1:qnb3YlIJmuetVBtC6Lsejr0Xru+1DNmDCdTqnwy7lhk=
github.com/CortexFoundation/torrentfs v1.0.20-0.20200810031954-d36d26f82fcc/go.mod h1:N5BsicP5ynjXIi/Npl/SRzlJ630n1PJV2sRj0Z0t2HA=
-github.com/CortexFoundation/torrentfs v1.0.73-0.20251217130652-29bcb4ed05d5 h1:uXwfi1WI24H7jrpZRTz6PE4hDivtyxH8KtKWPJfYxuI=
-github.com/CortexFoundation/torrentfs v1.0.73-0.20251217130652-29bcb4ed05d5/go.mod h1:t0r+P2JTv5z7zXQ0acSgiTpaeaQSm34GETHHAb1tWbE=
+github.com/CortexFoundation/torrentfs v1.0.73-0.20251221124821-bba7040b393f h1:MGnkO4YxORaZP0A2+XZkzVRF4GATdGer+/w53q2ZpT8=
+github.com/CortexFoundation/torrentfs v1.0.73-0.20251221124821-bba7040b393f/go.mod h1:mnSXvhnixWcj8YUCVAB1PvKvGryx8Vbj+AMshrPyMeI=
github.com/CortexFoundation/wormhole v0.0.2-0.20250807143819-52807b74f358 h1:y0QMrHsFxmrKBJDjYTnsXw8h/rtjO+tMnmK2OdUzZ/w=
github.com/CortexFoundation/wormhole v0.0.2-0.20250807143819-52807b74f358/go.mod h1:R/2T+BS27RdmRWWhoDdgSlordZpUBjVTh8hi4fHoioE=
github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
@@ -85,8 +85,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62 h1:Rge3uIIO891+nLqKTfMulCw+tWHtTl16Oudi0yUcAoE=
-github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549 h1:NERDcANvDCnspxdMEMLXOMnuITWIWrTQvvhEA8ewBBM=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w=
github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI=
@@ -617,8 +617,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
-github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU=
-github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/flatbuffers v25.12.19+incompatible h1:haMV2JRRJCe1998HeW/p0X9UaMTK6SDo0ffLn2+DbLs=
+github.com/google/flatbuffers v25.12.19+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@@ -748,8 +748,8 @@ github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
-github.com/jedib0t/go-pretty/v6 v6.7.7 h1:Y1Id3lJ3k4UB8uwWWy3l8EVFnUlx5chR5+VbsofPNX0=
-github.com/jedib0t/go-pretty/v6 v6.7.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
+github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o=
+github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7 h1:FWpSWRD8FbVkKQu8M1DM9jF5oXFLyE+XpisIYfdzbic=
github.com/jedisct1/go-minisign v0.0.0-20241212093149-d2f9f49435c7/go.mod h1:BMxO138bOokdgt4UaxZiEfypcSHX0t6SIFimVP1oRfk=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
@@ -913,8 +913,8 @@ github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOF
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
-github.com/nutsdb/nutsdb v1.0.4 h1:BurzkxijXJY1/AkIXe1ek+U1ta3WGi6nJt4nCLqkxQ8=
-github.com/nutsdb/nutsdb v1.0.4/go.mod h1:jIbbpBXajzTMZ0o33Yn5zoYIo3v0Dz4WstkVce+sYuQ=
+github.com/nutsdb/nutsdb v1.1.0 h1:fNGFzBHGqF2mB5BF8Qk8W94c3/ZzwdCdKAH7azwx70Y=
+github.com/nutsdb/nutsdb v1.1.0/go.mod h1:aKCtgSprZf2Mp1dIQD00Iya3DttoTErSSOnRx5ZtpAs=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@@ -1009,14 +1009,14 @@ github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.3.2/go.mod h1:q9wPnA96pu2urCcW/sK/RiDn597bhGoAQQ+y2fDwHuY=
github.com/pion/rtp v1.4.0/go.mod h1:/l4cvcKd0D3u9JLs2xSVI95YkfXW87a3br3nqmVtSlE=
-github.com/pion/rtp v1.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=
-github.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
+github.com/pion/rtp v1.8.27 h1:kbWTdZr62RDlYjatVAW4qFwrAu9XcGnwMsofCfAHlOU=
+github.com/pion/rtp v1.8.27/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8=
-github.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=
-github.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
+github.com/pion/sctp v1.9.0 h1:vajCA6G+1/SEi4vpPmDnpRNXwDNBmAXFBvJx0Le9HrI=
+github.com/pion/sctp v1.9.0/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=
github.com/pion/sdp/v2 v2.3.7/go.mod h1:+ZZf35r1+zbaWYiZLfPutWfx58DAWcGb2QsS3D/s9M8=
-github.com/pion/sdp/v3 v3.0.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=
-github.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
+github.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=
+github.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=
github.com/pion/srtp v1.3.1/go.mod h1:nxEytDDGTN+eNKJ1l5gzOCWQFuksgijorsSlgEjc40Y=
github.com/pion/srtp v1.3.2/go.mod h1:snPrfN+gVpRBpmats49oxLWfcFB01eH1N9F+N7+dxKI=
github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
@@ -1041,8 +1041,8 @@ github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=
github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=
github.com/pion/webrtc/v2 v2.2.7/go.mod h1:EfCuvKjzMgX4F/aSryRUC7L9o3u2N8WNUgnzd6wOO+8=
github.com/pion/webrtc/v2 v2.2.9/go.mod h1:TcArPDphZIBtZ+mh8J/qOREyY3ca7ihQrenulOIvfPQ=
-github.com/pion/webrtc/v4 v4.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=
-github.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=
+github.com/pion/webrtc/v4 v4.2.1 h1:QgIfJeXf9dg++35y4z8GK3oXHcxWf0y2tUstCry0/V8=
+github.com/pion/webrtc/v4 v4.2.1/go.mod h1:YDcAacHK1DZkkn1vwFn3yiXbixCBsEDaCNzg9PPAACk=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
@@ -1299,8 +1299,6 @@ github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 h1:FnBeRrxr7OU4VvAz
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xtaci/kcp-go v5.4.20+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
-github.com/xujiajun/mmap-go v1.0.1 h1:7Se7ss1fLPPRW+ePgqGpCkfGIZzJV6JPq9Wq9iv/WHc=
-github.com/xujiajun/mmap-go v1.0.1/go.mod h1:CNN6Sw4SL69Sui00p0zEzcZKbt+5HtEnYUsc6BKKRMg=
github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235 h1:w0si+uee0iAaCJO9q86T6yrhdadgcsoNuh47LrUykzg=
github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235/go.mod h1:MR4+0R6A9NS5IABnIM3384FfOq8QFVnm7WDrBOhIaMU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1393,8 +1391,8 @@ golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE
golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
-golang.org/x/exp v0.0.0-20251209150349-8475f28825e9 h1:MDfG8Cvcqlt9XXrmEiD4epKn7VJHZO84hejP9Jmp0MM=
-golang.org/x/exp v0.0.0-20251209150349-8475f28825e9/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
+golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
+golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -1516,7 +1514,6 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -1803,8 +1800,8 @@ modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
-modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
-modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
+modernc.org/libc v1.67.2 h1:ZbNmly1rcbjhot5jlOZG0q4p5VwFfjwWqZ5rY2xxOXo=
+modernc.org/libc v1.67.2/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -1813,8 +1810,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
-modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
-modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
+modernc.org/sqlite v1.41.0 h1:bJXddp4ZpsqMsNN1vS0jWo4IJTZzb8nWpcgvyCFG9Ck=
+modernc.org/sqlite v1.41.0/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
diff --git a/vendor/github.com/google/flatbuffers/go/encode.go b/vendor/github.com/google/flatbuffers/go/encode.go
index a2a5798125..b2f607a72c 100644
--- a/vendor/github.com/google/flatbuffers/go/encode.go
+++ b/vendor/github.com/google/flatbuffers/go/encode.go
@@ -25,7 +25,7 @@ func GetByte(buf []byte) byte {
// GetBool decodes a little-endian bool from a byte slice.
func GetBool(buf []byte) bool {
- return buf[0] == 1
+ return buf[0] != 0
}
// GetUint8 decodes a little-endian uint8 from a byte slice.
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/progress/README.md b/vendor/github.com/jedib0t/go-pretty/v6/progress/README.md
index 47ad2a3f5a..0db8fbe296 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/progress/README.md
+++ b/vendor/github.com/jedib0t/go-pretty/v6/progress/README.md
@@ -26,11 +26,17 @@ A demonstration of all the capabilities can be found here:
### Tracker Management
- Dynamically add one or more Task Trackers while `Render()` is in progress
- - Sort trackers by Message, Percentage, or Value (ascending/descending)
+ - Sort trackers by Index (ascending/descending), Message, Percentage, or Value
+ - `SortByIndex` / `SortByIndexDsc` - Sort by explicit Index field, maintaining
+ order regardless of completion status (done and active trackers are merged
+ and sorted together)
+ - For other sorting methods, done and active trackers are sorted separately,
+ with done trackers always rendered before active trackers
- Tracker options
+ - `AutoStopDisabled` - Prevent auto-completion when value exceeds total
- `DeferStart` - Delay tracker start until manually triggered
+ - `Index` - Explicit ordering value for trackers (used with `SortByIndex`)
- `RemoveOnCompletion` - Hide tracker when done instead of showing completion
- - `AutoStopDisabled` - Prevent auto-completion when value exceeds total
### Display & Rendering
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/progress/indicator.go b/vendor/github.com/jedib0t/go-pretty/v6/progress/indicator.go
index 9994433359..13bf790091 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/progress/indicator.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/progress/indicator.go
@@ -155,9 +155,10 @@ func indeterminateIndicatorDominoes() IndeterminateIndicatorGenerator {
return func(maxLen int) IndeterminateIndicator {
currentPosition := nextPosition
- if currentPosition == 0 {
+ switch currentPosition {
+ case 0:
direction = 1
- } else if currentPosition == maxLen {
+ case maxLen:
direction = -1
}
nextPosition += direction
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/progress/progress.go b/vendor/github.com/jedib0t/go-pretty/v6/progress/progress.go
index 361a293154..bad9a05a0c 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/progress/progress.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/progress/progress.go
@@ -32,6 +32,7 @@ type Progress struct {
logsToRenderMutex sync.RWMutex
numTrackersExpected int64
outputWriter io.Writer
+ outputWriterMutex sync.RWMutex
overallTracker *Tracker
overallTrackerMutex sync.RWMutex
pinnedMessages []string
@@ -199,7 +200,9 @@ func (p *Progress) SetNumTrackersExpected(numTrackers int) {
// os.Stdout or os.Stderr or a file. Warning: redirecting the output to a file
// may not work well as the Render() logic moves the cursor around a lot.
func (p *Progress) SetOutputWriter(writer io.Writer) {
+ p.outputWriterMutex.Lock()
p.outputWriter = writer
+ p.outputWriterMutex.Unlock()
}
// SetPinnedMessages sets message(s) pinned above all the trackers of the
@@ -344,16 +347,19 @@ func (p *Progress) initForRender() {
}
// if not output write has been set, output to STDOUT
+ p.outputWriterMutex.RLock()
if p.outputWriter == nil {
p.outputWriter = os.Stdout
}
+ outputWriter := p.outputWriter
+ p.outputWriterMutex.RUnlock()
// pick a sane update frequency if none set
if p.updateFrequency <= 0 {
p.updateFrequency = DefaultUpdateFrequency
}
- if p.outputWriter == os.Stdout {
+ if outputWriter == os.Stdout {
// get the current terminal size for preventing roll-overs, and do this in a
// background loop until end of render. This only works if the output writer is STDOUT.
go p.watchTerminalSize() // needs p.updateFrequency
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/progress/render.go b/vendor/github.com/jedib0t/go-pretty/v6/progress/render.go
index f79d89e799..99bef445be 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/progress/render.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/progress/render.go
@@ -44,6 +44,43 @@ func (p *Progress) beginRender() bool {
return true
}
+func (p *Progress) collectActiveTrackers() ([]*Tracker, int64, time.Duration) {
+ var allTrackers []*Tracker
+ var activeTrackersProgress int64
+ var maxETA time.Duration
+
+ p.trackersDoneMutex.RLock()
+ lengthDone := len(p.trackersDone)
+ p.trackersDoneMutex.RUnlock()
+
+ p.trackersActiveMutex.RLock()
+ allTrackers = make([]*Tracker, 0, len(p.trackersActive)+lengthDone)
+ for _, tracker := range p.trackersActive {
+ if !tracker.IsDone() || !tracker.RemoveOnCompletion {
+ allTrackers = append(allTrackers, tracker)
+ if !tracker.IsDone() {
+ activeTrackersProgress += int64(tracker.PercentDone())
+ if eta := tracker.ETA(); eta > maxETA {
+ maxETA = eta
+ }
+ }
+ }
+ }
+ p.trackersActiveMutex.RUnlock()
+
+ return allTrackers, activeTrackersProgress, maxETA
+}
+
+func (p *Progress) collectDoneTrackers(allTrackers *[]*Tracker) {
+ p.trackersDoneMutex.RLock()
+ for _, tracker := range p.trackersDone {
+ if !tracker.RemoveOnCompletion {
+ *allTrackers = append(*allTrackers, tracker)
+ }
+ }
+ p.trackersDoneMutex.RUnlock()
+}
+
func (p *Progress) consumeQueuedTrackers() {
p.trackersInQueueMutex.Lock()
queueLen := len(p.trackersInQueue)
@@ -70,48 +107,24 @@ func (p *Progress) endRender() {
p.renderInProgress = false
}
-func (p *Progress) extractDoneAndActiveTrackers() ([]*Tracker, []*Tracker) {
+// extractAllTrackersInOrder extracts all trackers (both active and done) and
+// sorts them together when SortByIndex is used. This allows maintaining a fixed
+// order regardless of completion status.
+func (p *Progress) extractAllTrackersInOrder() []*Tracker {
// move trackers waiting in queue to the active list
p.consumeQueuedTrackers()
- // separate the active and done trackers
- var trackersActive, trackersDone []*Tracker
- var activeTrackersProgress int64
- var maxETA time.Duration
- var lengthDone int
+ allTrackers, activeTrackersProgress, maxETA := p.collectActiveTrackers()
+ p.collectDoneTrackers(&allTrackers)
- // Get lengthDone while we have access to trackersDone
- p.trackersDoneMutex.RLock()
- lengthDone = len(p.trackersDone)
- p.trackersDoneMutex.RUnlock()
-
- p.trackersActiveMutex.RLock()
- // Pre-allocate slices with estimated capacity to reduce allocations
- trackersActive = make([]*Tracker, 0, len(p.trackersActive))
- trackersDone = make([]*Tracker, 0, len(p.trackersActive)/4) // estimate ~25% done
- for _, tracker := range p.trackersActive {
- if !tracker.IsDone() {
- trackersActive = append(trackersActive, tracker)
- activeTrackersProgress += int64(tracker.PercentDone())
- if eta := tracker.ETA(); eta > maxETA {
- maxETA = eta
- }
- } else if !tracker.RemoveOnCompletion {
- trackersDone = append(trackersDone, tracker)
- }
+ // Sort by Index (ascending or descending)
+ if p.sortBy == SortByIndex || p.sortBy == SortByIndexDsc {
+ p.sortBy.Sort(allTrackers)
}
- p.trackersActiveMutex.RUnlock()
- p.sortBy.Sort(trackersDone)
- p.sortBy.Sort(trackersActive)
- // calculate the overall tracker's progress value
- p.overallTracker.value = int64(lengthDone+len(trackersDone)) * 100
- p.overallTracker.value += activeTrackersProgress
- p.overallTracker.minETA = maxETA
- if len(trackersActive) == 0 {
- p.overallTracker.MarkAsDone()
- }
- return trackersActive, trackersDone
+ p.updateOverallTrackerProgress(allTrackers, activeTrackersProgress, maxETA)
+
+ return allTrackers
}
func (p *Progress) generateTrackerStr(t *Tracker, maxLen int, hint renderHint) string {
@@ -191,7 +204,28 @@ func (p *Progress) generateTrackerStrIndeterminate(maxLen int) string {
}
func (p *Progress) moveCursorToTheTop(out *strings.Builder) {
- numLinesToMoveUp := len(p.trackersActive)
+ // Count all trackers that will be rendered (both done and active)
+ var numTrackersToRender int
+
+ // Count active trackers (excluding those with RemoveOnCompletion)
+ p.trackersActiveMutex.RLock()
+ for _, tracker := range p.trackersActive {
+ if !tracker.RemoveOnCompletion {
+ numTrackersToRender++
+ }
+ }
+ p.trackersActiveMutex.RUnlock()
+
+ // Count done trackers (excluding those with RemoveOnCompletion)
+ p.trackersDoneMutex.RLock()
+ for _, tracker := range p.trackersDone {
+ if !tracker.RemoveOnCompletion {
+ numTrackersToRender++
+ }
+ }
+ p.trackersDoneMutex.RUnlock()
+
+ numLinesToMoveUp := numTrackersToRender
if p.style.Visibility.TrackerOverall && p.overallTracker != nil && !p.overallTracker.IsDone() {
numLinesToMoveUp++
}
@@ -348,28 +382,55 @@ func (p *Progress) renderTrackers(lastRenderLength int) int {
}
// write the text to the output writer
+ p.outputWriterMutex.Lock()
_, _ = p.outputWriter.Write([]byte(out.String()))
+ p.outputWriterMutex.Unlock()
// stop if auto stop is enabled and there are no more active trackers
if p.autoStop && p.LengthActive() == 0 {
- p.renderContextCancel()
+ p.renderContextCancelMutex.Lock()
+ if p.renderContextCancel != nil {
+ p.renderContextCancel()
+ }
+ p.renderContextCancelMutex.Unlock()
}
return out.Len()
}
func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder, hint renderHint) {
- // find the currently "active" and "done" trackers
- trackersActive, trackersDone := p.extractDoneAndActiveTrackers()
+ // Extract all trackers (both active and done)
+ allTrackers := p.extractAllTrackersInOrder()
+
+ // Separate done and active trackers for sorting and state management
+ trackersDone, trackersActive := p.separateDoneAndActiveTrackers(allTrackers)
- // sort and render the done trackers
- for _, tracker := range trackersDone {
+ // Sort trackers based on sortBy setting
+ trackersToRender := p.sortTrackersForRendering(allTrackers, trackersDone, trackersActive)
+
+ // Render all trackers in the determined order
+ for _, tracker := range trackersToRender {
p.renderTracker(out, tracker, hint)
}
+
+ // Update internal state
p.trackersDoneMutex.Lock()
- p.trackersDone = append(p.trackersDone, trackersDone...)
+ // Only add newly done trackers that aren't already in trackersDone
+ existingDone := make(map[*Tracker]bool)
+ for _, t := range p.trackersDone {
+ existingDone[t] = true
+ }
+ for _, t := range trackersDone {
+ if !existingDone[t] {
+ p.trackersDone = append(p.trackersDone, t)
+ }
+ }
p.trackersDoneMutex.Unlock()
+ p.trackersActiveMutex.Lock()
+ p.trackersActive = trackersActive
+ p.trackersActiveMutex.Unlock()
+
// render all the logs received and flush them out
p.logsToRenderMutex.Lock()
for _, log := range p.logsToRender {
@@ -384,14 +445,6 @@ func (p *Progress) renderTrackersDoneAndActive(out *strings.Builder, hint render
if len(trackersActive) > 0 && p.style.Visibility.Pinned {
p.renderPinnedMessages(out, hint)
}
-
- // sort and render the active trackers
- for _, tracker := range trackersActive {
- p.renderTracker(out, tracker, hint)
- }
- p.trackersActiveMutex.Lock()
- p.trackersActive = trackersActive
- p.trackersActiveMutex.Unlock()
}
func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint renderHint) {
@@ -420,6 +473,23 @@ func (p *Progress) renderTrackerStats(out *strings.Builder, t *Tracker, hint ren
}
}
+func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) {
+ if hint.isOverallTracker && !p.style.Visibility.ETAOverall {
+ return
+ }
+ if !hint.isOverallTracker && !p.style.Visibility.ETA {
+ return
+ }
+
+ tpETA := p.style.Options.ETAPrecision
+ if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA {
+ out.WriteString("; ")
+ out.WriteString(p.style.Options.ETAString)
+ out.WriteString(": ")
+ out.WriteString(p.style.Colors.Time.Sprint(eta))
+ }
+}
+
func (p *Progress) renderTrackerStatsSpeed(out *strings.Builder, t *Tracker, hint renderHint) {
if hint.isOverallTracker && !p.style.Visibility.SpeedOverall {
return
@@ -434,8 +504,9 @@ func (p *Progress) renderTrackerStatsSpeed(out *strings.Builder, t *Tracker, hin
p.trackersActiveMutex.RLock()
for _, tracker := range p.trackersActive {
- if !tracker.timeStart.IsZero() {
- speed += float64(tracker.Value()) / time.Since(tracker.timeStart).Round(speedPrecision).Seconds()
+ timeStart := tracker.timeStartValue()
+ if !timeStart.IsZero() {
+ speed += float64(tracker.Value()) / time.Since(timeStart).Round(speedPrecision).Seconds()
}
}
p.trackersActiveMutex.RUnlock()
@@ -443,10 +514,13 @@ func (p *Progress) renderTrackerStatsSpeed(out *strings.Builder, t *Tracker, hin
if speed > 0 {
p.renderTrackerStatsSpeedInternal(out, p.style.Options.SpeedOverallFormatter(int64(speed)))
}
- } else if !t.timeStart.IsZero() {
- timeTaken := time.Since(t.timeStart)
- if timeTakenRounded := timeTaken.Round(speedPrecision); timeTakenRounded > speedPrecision {
- p.renderTrackerStatsSpeedInternal(out, t.Units.Sprint(int64(float64(t.Value())/timeTakenRounded.Seconds())))
+ } else {
+ timeStart := t.timeStartValue()
+ if !timeStart.IsZero() {
+ timeTaken := time.Since(timeStart)
+ if timeTakenRounded := timeTaken.Round(speedPrecision); timeTakenRounded > speedPrecision {
+ p.renderTrackerStatsSpeedInternal(out, t.Units.Sprint(int64(float64(t.Value())/timeTakenRounded.Seconds())))
+ }
}
}
}
@@ -464,11 +538,12 @@ func (p *Progress) renderTrackerStatsSpeedInternal(out *strings.Builder, speed s
func (p *Progress) renderTrackerStatsTime(outStats *strings.Builder, t *Tracker, hint renderHint) {
var td, tp time.Duration
- if !t.timeStart.IsZero() {
+ timeStart, timeStop := t.timeStartAndStop()
+ if !timeStart.IsZero() {
if t.IsDone() {
- td = t.timeStop.Sub(t.timeStart)
+ td = timeStop.Sub(timeStart)
} else {
- td = time.Since(t.timeStart)
+ td = time.Since(timeStart)
}
}
if hint.isOverallTracker {
@@ -483,19 +558,43 @@ func (p *Progress) renderTrackerStatsTime(outStats *strings.Builder, t *Tracker,
p.renderTrackerStatsETA(outStats, t, hint)
}
-func (p *Progress) renderTrackerStatsETA(out *strings.Builder, t *Tracker, hint renderHint) {
- if hint.isOverallTracker && !p.style.Visibility.ETAOverall {
- return
+func (p *Progress) separateDoneAndActiveTrackers(allTrackers []*Tracker) ([]*Tracker, []*Tracker) {
+ var trackersDone, trackersActive []*Tracker
+ for _, tracker := range allTrackers {
+ if tracker.IsDone() {
+ if !tracker.RemoveOnCompletion {
+ trackersDone = append(trackersDone, tracker)
+ }
+ } else {
+ trackersActive = append(trackersActive, tracker)
+ }
}
- if !hint.isOverallTracker && !p.style.Visibility.ETA {
- return
+ return trackersDone, trackersActive
+}
+
+func (p *Progress) sortTrackersForRendering(allTrackers []*Tracker, trackersDone []*Tracker, trackersActive []*Tracker) []*Tracker {
+ if p.sortBy == SortByIndex || p.sortBy == SortByIndexDsc {
+ // For explicit index ordering (ascending or descending), all trackers are already sorted together
+ return allTrackers
}
+ // For other sort methods, sort done and active separately, then combine
+ p.sortBy.Sort(trackersDone)
+ p.sortBy.Sort(trackersActive)
+ // Combine: done first, then active
+ return append(trackersDone, trackersActive...)
+}
- tpETA := p.style.Options.ETAPrecision
- if eta := t.ETA().Round(tpETA); hint.isOverallTracker || eta > tpETA {
- out.WriteString("; ")
- out.WriteString(p.style.Options.ETAString)
- out.WriteString(": ")
- out.WriteString(p.style.Colors.Time.Sprint(eta))
+func (p *Progress) updateOverallTrackerProgress(allTrackers []*Tracker, activeTrackersProgress int64, maxETA time.Duration) {
+ doneCount := 0
+ for _, tracker := range allTrackers {
+ if tracker.IsDone() {
+ doneCount++
+ }
+ }
+ p.overallTracker.value = int64(doneCount) * 100
+ p.overallTracker.value += activeTrackersProgress
+ p.overallTracker.minETA = maxETA
+ if len(allTrackers) > 0 && doneCount == len(allTrackers) {
+ p.overallTracker.MarkAsDone()
}
}
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker.go b/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker.go
index 164373bb47..1e775c57e5 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker.go
@@ -16,11 +16,6 @@ type Tracker struct {
// the value goes beyond the total (if set). Note that this means that a
// manual call to MarkAsDone or MarkAsErrored is expected.
AutoStopDisabled bool
- // Message should contain a short description of the "task"; please note
- // that this should NOT be updated in the middle of progress - you should
- // instead use UpdateMessage() to do this safely without hitting any race
- // conditions
- Message string
// DeferStart prevents the tracker from starting immediately when appended.
// It will be rendered but remain dormant until Start, Increment,
// IncrementWithError or SetValue is called.
@@ -28,6 +23,15 @@ type Tracker struct {
// ExpectedDuration tells how long this task is expected to take; and will
// be used in calculation of the ETA value
ExpectedDuration time.Duration
+ // Index specifies the explicit order for this tracker. When SortByIndex
+ // is used, trackers are sorted by this value regardless of completion status.
+ // Lower values appear first, with 0 being the first index.
+ Index uint64
+ // Message should contain a short description of the "task"; please note
+ // that this should NOT be updated in the middle of progress - you should
+ // instead use UpdateMessage() to do this safely without hitting any race
+ // conditions
+ Message string
// RemoveOnCompletion tells the Progress Bar to remove this tracker when
// it is done, instead of rendering a "completed" line
RemoveOnCompletion bool
@@ -260,3 +264,20 @@ func (t *Tracker) valueAndTotal() (int64, int64) {
t.mutex.RUnlock()
return value, total
}
+
+// timeStartAndStop returns the start and stop times safely.
+func (t *Tracker) timeStartAndStop() (time.Time, time.Time) {
+ t.mutex.RLock()
+ timeStart := t.timeStart
+ timeStop := t.timeStop
+ t.mutex.RUnlock()
+ return timeStart, timeStop
+}
+
+// timeStartValue returns the start time safely.
+func (t *Tracker) timeStartValue() time.Time {
+ t.mutex.RLock()
+ timeStart := t.timeStart
+ t.mutex.RUnlock()
+ return timeStart
+}
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker_sort.go b/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker_sort.go
index aa998b9772..3648f0043a 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker_sort.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/progress/tracker_sort.go
@@ -9,6 +9,16 @@ const (
// SortByNone doesn't do any sorting == sort by insertion order.
SortByNone SortBy = iota
+ // SortByIndex sorts by the Index field in ascending order. When this is used,
+ // trackers are rendered in Index order regardless of completion status (done
+ // and active trackers are merged and sorted together). Index 0 comes first.
+ SortByIndex
+
+ // SortByIndexDsc sorts by the Index field in descending order. When this is used,
+ // trackers are rendered in Index order regardless of completion status (done
+ // and active trackers are merged and sorted together). Higher indices come first.
+ SortByIndexDsc
+
// SortByMessage sorts by the Message alphabetically in ascending order.
SortByMessage
@@ -31,23 +41,52 @@ const (
// Sort applies the sorting method defined by SortBy.
func (sb SortBy) Sort(trackers []*Tracker) {
switch sb {
+ case SortByIndex:
+ sort.Sort(sortByIndex(trackers))
+ case SortByIndexDsc:
+ sort.Stable(sortByIndexDsc(trackers))
case SortByMessage:
sort.Sort(sortByMessage(trackers))
case SortByMessageDsc:
sort.Sort(sortDsc{sortByMessage(trackers)})
case SortByPercent:
- sort.Sort(sortByPercent(trackers))
+ sort.Stable(sortByPercent(trackers))
case SortByPercentDsc:
- sort.Sort(sortDsc{sortByPercent(trackers)})
+ sort.Stable(sortByPercentDsc(trackers))
case SortByValue:
- sort.Sort(sortByValue(trackers))
+ sort.Stable(sortByValue(trackers))
case SortByValueDsc:
- sort.Sort(sortDsc{sortByValue(trackers)})
+ sort.Stable(sortByValueDsc(trackers))
default:
// no sort
}
}
+type sortByIndex []*Tracker
+
+func (sb sortByIndex) Len() int { return len(sb) }
+func (sb sortByIndex) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] }
+func (sb sortByIndex) Less(i, j int) bool {
+ if sb[i].Index == sb[j].Index {
+ // Same index: maintain insertion order (use timeStart as tiebreaker)
+ return sb[i].timeStartValue().Before(sb[j].timeStartValue())
+ }
+ return sb[i].Index < sb[j].Index
+}
+
+type sortByIndexDsc []*Tracker
+
+func (sb sortByIndexDsc) Len() int { return len(sb) }
+func (sb sortByIndexDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] }
+func (sb sortByIndexDsc) Less(i, j int) bool {
+ if sb[i].Index == sb[j].Index {
+ // Same index: maintain insertion order (earlier timeStart first)
+ return sb[i].timeStartValue().Before(sb[j].timeStartValue())
+ }
+ // Reverse: higher index comes first
+ return sb[i].Index > sb[j].Index
+}
+
type sortByMessage []*Tracker
func (sb sortByMessage) Len() int { return len(sb) }
@@ -60,22 +99,57 @@ func (sb sortByPercent) Len() int { return len(sb) }
func (sb sortByPercent) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] }
func (sb sortByPercent) Less(i, j int) bool {
if sb[i].PercentDone() == sb[j].PercentDone() {
- return sb[i].timeStart.Before(sb[j].timeStart)
+ // When percentages are equal, preserve insertion order (earlier timeStart first)
+ return sb[i].timeStartValue().Before(sb[j].timeStartValue())
}
return sb[i].PercentDone() < sb[j].PercentDone()
}
+type sortByPercentDsc []*Tracker
+
+func (sb sortByPercentDsc) Len() int { return len(sb) }
+func (sb sortByPercentDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] }
+func (sb sortByPercentDsc) Less(i, j int) bool {
+ if sb[i].PercentDone() == sb[j].PercentDone() {
+ // When percentages are equal, preserve insertion order (earlier timeStart first)
+ return sb[i].timeStartValue().Before(sb[j].timeStartValue())
+ }
+ // Reverse: higher percentage comes first
+ return sb[i].PercentDone() > sb[j].PercentDone()
+}
+
type sortByValue []*Tracker
func (sb sortByValue) Len() int { return len(sb) }
func (sb sortByValue) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] }
func (sb sortByValue) Less(i, j int) bool {
- if sb[i].value == sb[j].value {
- return sb[i].timeStart.Before(sb[j].timeStart)
+ valueI := sb[i].Value()
+ valueJ := sb[j].Value()
+ if valueI == valueJ {
+ return sb[i].timeStartValue().Before(sb[j].timeStartValue())
}
- return sb[i].value < sb[j].value
+ return valueI < valueJ
+}
+
+type sortByValueDsc []*Tracker
+
+func (sb sortByValueDsc) Len() int { return len(sb) }
+func (sb sortByValueDsc) Swap(i, j int) { sb[i], sb[j] = sb[j], sb[i] }
+func (sb sortByValueDsc) Less(i, j int) bool {
+ valueI := sb[i].Value()
+ valueJ := sb[j].Value()
+ if valueI == valueJ {
+ // When values are equal, preserve insertion order (earlier timeStart first)
+ return sb[i].timeStartValue().Before(sb[j].timeStartValue())
+ }
+ // Reverse: higher value comes first
+ return valueI > valueJ
}
type sortDsc struct{ sort.Interface }
-func (sd sortDsc) Less(i, j int) bool { return !sd.Interface.Less(i, j) && sd.Interface.Less(j, i) }
+func (sd sortDsc) Less(i, j int) bool {
+ // Reverse the comparison for descending order
+ // When elements are equal (both Less calls return false), preserve insertion order
+ return !sd.Interface.Less(i, j) && sd.Interface.Less(j, i)
+}
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/ansi.go b/vendor/github.com/jedib0t/go-pretty/v6/text/ansi.go
index 6f13b656a8..8436ffabd3 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/text/ansi.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/text/ansi.go
@@ -15,18 +15,22 @@ var ANSICodesSupported = areANSICodesSupported()
// Escape("Nymeria\x1b[94mGhost\x1b[0mLady", "\x1b[91m") == "\x1b[91mNymeria\x1b[94mGhost\x1b[0m\x1b[91mLady\x1b[0m"
// Escape("Nymeria \x1b[94mGhost\x1b[0m Lady", "\x1b[91m") == "\x1b[91mNymeria \x1b[94mGhost\x1b[0m\x1b[91m Lady\x1b[0m"
func Escape(str string, escapeSeq string) string {
- out := ""
+ var out strings.Builder
+ // Estimate capacity: original string + escape sequences
+ out.Grow(len(str) + len(escapeSeq)*3 + len(EscapeReset)*2)
+
if !strings.HasPrefix(str, EscapeStart) {
- out += escapeSeq
+ out.WriteString(escapeSeq)
}
- out += strings.Replace(str, EscapeReset, EscapeReset+escapeSeq, -1)
- if !strings.HasSuffix(out, EscapeReset) {
- out += EscapeReset
+ out.WriteString(strings.ReplaceAll(str, EscapeReset, EscapeReset+escapeSeq))
+ if !strings.HasSuffix(out.String(), EscapeReset) {
+ out.WriteString(EscapeReset)
}
- if strings.Contains(out, escapeSeq+EscapeReset) {
- out = strings.Replace(out, escapeSeq+EscapeReset, "", -1)
+ result := out.String()
+ if strings.Contains(result, escapeSeq+EscapeReset) {
+ result = strings.ReplaceAll(result, escapeSeq+EscapeReset, "")
}
- return out
+ return result
}
// StripEscape strips all ANSI Escape Sequence from the string.
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/valign.go b/vendor/github.com/jedib0t/go-pretty/v6/text/valign.go
index f1a75e96d5..8e086d92d2 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/text/valign.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/text/valign.go
@@ -27,10 +27,11 @@ func (va VAlign) Apply(lines []string, maxLines int) []string {
maxLines = len(lines)
}
- insertIdx := 0
- if va == VAlignMiddle {
+ var insertIdx int
+ switch va {
+ case VAlignMiddle:
insertIdx = int(maxLines-len(lines)) / 2
- } else if va == VAlignBottom {
+ case VAlignBottom:
insertIdx = maxLines - len(lines)
}
diff --git a/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go b/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go
index 8a9e803de9..fd657f79b0 100644
--- a/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go
+++ b/vendor/github.com/jedib0t/go-pretty/v6/text/wrap.go
@@ -13,7 +13,7 @@ func WrapHard(str string, wrapLen int) string {
if wrapLen <= 0 {
return ""
}
- str = strings.Replace(str, "\t", " ", -1)
+ str = strings.ReplaceAll(str, "\t", " ")
sLen := StringWidthWithoutEscSequences(str)
if sLen <= wrapLen {
return str
@@ -41,7 +41,7 @@ func WrapSoft(str string, wrapLen int) string {
if wrapLen <= 0 {
return ""
}
- str = strings.Replace(str, "\t", " ", -1)
+ str = strings.ReplaceAll(str, "\t", " ")
sLen := StringWidthWithoutEscSequences(str)
if sLen <= wrapLen {
return str
@@ -68,7 +68,7 @@ func WrapText(str string, wrapLen int) string {
if wrapLen <= 0 {
return ""
}
- str = strings.Replace(str, "\t", " ", -1)
+ str = strings.ReplaceAll(str, "\t", " ")
sLen := StringWidthWithoutEscSequences(str)
if sLen <= wrapLen {
return str
diff --git a/vendor/github.com/nutsdb/nutsdb/.gitignore b/vendor/github.com/nutsdb/nutsdb/.gitignore
index cfab6cb732..19022fd642 100644
--- a/vendor/github.com/nutsdb/nutsdb/.gitignore
+++ b/vendor/github.com/nutsdb/nutsdb/.gitignore
@@ -1,4 +1,8 @@
.idea/
testdata/
-
-*/*.DS_Store
\ No newline at end of file
+bin/
+*/*.DS_Store
+.gocache
+.vscode
+coverage.out
+coverage.html
diff --git a/vendor/github.com/nutsdb/nutsdb/.travis.yml b/vendor/github.com/nutsdb/nutsdb/.travis.yml
deleted file mode 100644
index 79cf410571..0000000000
--- a/vendor/github.com/nutsdb/nutsdb/.travis.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-language: go
-go:
- - 1.11.x
- - tip
-before_install:
- - go get golang.org/x/tools/cmd/cover
- - go get github.com/mattn/goveralls
-script:
- - go test -v -covermode=count -coverprofile=coverage.out
- - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci
\ No newline at end of file
diff --git a/vendor/github.com/nutsdb/nutsdb/README-CN.md b/vendor/github.com/nutsdb/nutsdb/README-CN.md
index 7ea097a950..abaad74e77 100644
--- a/vendor/github.com/nutsdb/nutsdb/README-CN.md
+++ b/vendor/github.com/nutsdb/nutsdb/README-CN.md
@@ -2,7 +2,7 @@
-# NutsDB [](https://godoc.org/github.com/nutsdb/nutsdb) [](https://goreportcard.com/report/github.com/nutsdb/nutsdb) [](https://github.com/nutsdb/nutsdb/actions) [](https://codecov.io/gh/nutsdb/nutsdb) [](https://raw.githubusercontent.com/nutsdb/nutsdb/master/LICENSE) [](https://github.com/avelino/awesome-go#database)
+# NutsDB [](https://godoc.org/github.com/nutsdb/nutsdb) [](https://goreportcard.com/report/github.com/nutsdb/nutsdb) [](https://github.com/nutsdb/nutsdb/actions/workflows/go.yml) [](https://codecov.io/gh/nutsdb/nutsdb) [](https://raw.githubusercontent.com/nutsdb/nutsdb/master/LICENSE) [](https://github.com/avelino/awesome-go#database)
[English](https://github.com/nutsdb/nutsdb/blob/master/README.md) | 简体中文
@@ -12,7 +12,7 @@ NutsDB 是一个用纯 Go 编写的简单、快速、可嵌入且持久的键/
我们可以在NutsDB的文档网站了解更多:[NutsDB Documents](https://nutsdb.github.io/nutsdb-docs/)
-欢迎对NutsDB感兴趣的加群、一起开发,具体看这个issue:https://github.com/nutsdb/nutsdb/issues/116。
+欢迎对NutsDB感兴趣的加群、一起开发,具体看这个issue:https://github.com/nutsdb/nutsdb/issues/116 。
### 关注nutsdb公众号

-

+

@@ -77,6 +77,226 @@ func main() {
}
```
+## Merge V2 and HintFile
+
+### Overview
+
+NutsDB introduces **Merge V2** and **HintFile** features to significantly improve database performance, especially for startup time and memory efficiency during large-scale compaction operations.
+
+### Merge V2
+
+**Merge V2** is a high-performance compaction algorithm that optimizes memory usage while maintaining data consistency. It's designed to handle large-scale data compaction efficiently.
+
+#### Key Features
+
+- **Memory Efficiency**: Reduces memory usage by ~65% during merge operations (from ~145 bytes/entry to ~50 bytes/entry)
+- **Concurrent Safety**: Allows concurrent writes during merge operations
+- **Atomic Operations**: Ensures data consistency through atomic commit and rollback
+- **Large-Scale Support**: Optimized for handling 10GB+ datasets efficiently
+
+#### How It Works
+
+1. **Preparation Phase**: Enumerates files and validates merge state
+2. **Rewrite Phase**: Processes data files and rewrites valid entries to new merge files
+3. **Commit Phase**: Updates in-memory indexes and writes hint files
+4. **Finalization**: Ensures all data is persisted and cleans up old files
+
+#### File ID Strategy
+
+Merge V2 uses negative FileIDs for merge files to ensure correct processing order:
+- Merge files: Negative IDs (starting from `math.MinInt64`)
+- Normal data files: Positive IDs (starting from 0)
+- Processing order: Merge files are always processed before normal files during index rebuild
+
+#### Configuration
+
+```go
+// Merge V2 is enabled by default and works automatically
+// You can configure merge behavior through options
+
+options := nutsdb.DefaultOptions
+options.SegmentSize = 256 * 1024 * 1024 // 256MB segments
+
+db, err := nutsdb.Open(options, nutsdb.WithDir("/tmp/nutsdb"))
+```
+
+### HintFile
+
+**HintFile** is an index persistence feature that dramatically reduces database startup time by maintaining persistent indexes of key metadata.
+
+#### Key Features
+
+- **Fast Startup**: Eliminates full data file scans during database recovery
+- **Automatic Fallback**: Gracefully falls back to traditional scanning if hint files are missing or corrupted
+- **Transparent Operation**: Works seamlessly with existing data structures
+- **Configurable**: Can be enabled/disabled via configuration
+
+#### How It Works
+
+1. **During Merge**: Hint files are automatically created alongside merge data files
+2. **During Startup**: Database loads hint files to reconstruct in-memory indexes instantly
+3. **Fallback Mechanism**: If hint files are unavailable, the system falls back to scanning data files
+
+#### Hint File Structure
+
+Each hint entry contains:
+- Bucket ID, Key metadata, Value size
+- Timestamp, TTL, and operation flags
+- File ID and data position for direct access
+- Key data for lookup operations
+
+#### Configuration
+
+```go
+// Basic configuration with both features enabled
+options := nutsdb.DefaultOptions
+options.EnableHintFile = true
+options.EnableMergeV2 = true
+options.SegmentSize = 256 * 1024 * 1024 // 256MB segments
+
+// Or use option functions
+db, err := nutsdb.Open(
+ nutsdb.DefaultOptions,
+ nutsdb.WithDir("/tmp/nutsdb"),
+ nutsdb.WithEnableHintFile(true),
+ nutsdb.WithEnableMergeV2(true),
+)
+```
+
+#### Configuration Options
+
+| Option | Type | Default | Description |
+|--------|------|---------|-------------|
+| `EnableHintFile` | bool | `false` | Enable/disable hint file feature |
+| `EnableMergeV2` | bool | `false` | Enable/disable Merge V2 algorithm |
+| `SegmentSize` | int64 | `256MB` | Size of data files before rotation |
+
+### Performance Benefits
+
+#### Memory Efficiency
+- **Traditional Merge**: ~145 bytes per entry
+- **Merge V2**: ~50 bytes per entry
+- **Savings**: ~65% memory reduction for large datasets
+
+#### Startup Time
+- **With HintFile**: Index reconstruction in milliseconds
+- **Without HintFile**: Full data file scanning (seconds to minutes)
+- **Fallback**: Automatic degradation if hint files are unavailable
+
+#### Benchmark Results
+
+Performance testing with Intel Core i5-14600KF on large datasets:
+
+| Configuration | Total Time | Memory Usage | Allocations | Avg Restart | Merge Time |
+|---------------|------------|--------------|-------------|-------------|------------|
+| Merge V1 + No HintFile | 4.29s | 2.32GB | 40.4M | 4.27s | 12.86s |
+| Merge V1 + HintFile | 1.69s | 1.56GB | 25.4M | 1.64s | 16.37s |
+| **Merge V2 + HintFile** | **1.87s** | **1.56GB** | **25.4M** | **1.83s** | **8.63s** |
+
+##### Key Performance Insights
+
+- **HintFile Impact**: 61.6% faster startup time (4.27s → 1.64s)
+- **Memory Optimization**: 32.9% reduction in memory allocation with HintFile
+- **Merge V2 Efficiency**: 47.3% faster merge operations (16.37s → 8.63s)
+- **Overall Performance**: Best balance with Merge V2 + HintFile combination
+
+#### Use Cases
+- **Large Datasets**: Essential for databases with millions of entries
+- **Frequent Restarts**: Critical for applications requiring fast restarts
+- **Memory-Constrained Environments**: Reduces peak memory usage during maintenance
+- **High Availability**: Enables quick database recovery after failures
+
+### Best Practices
+
+1. **Enable HintFile** for production workloads with large datasets
+2. **Monitor Disk Space** - Hint files add additional storage overhead
+3. **Regular Merges** - Schedule merges during low-traffic periods
+4. **Backup Strategy** - Include hint files in your backup process
+
+### Migration Guide
+
+#### Enabling HintFile in Existing Database
+
+```go
+// Step 1: Backup your database
+// Step 2: Enable HintFile and perform a merge to create hint files
+options := nutsdb.DefaultOptions
+options.EnableHintFile = true
+options.EnableMergeV2 = true
+
+db, err := nutsdb.Open(options)
+if err != nil {
+ log.Fatal(err)
+}
+
+// Step 3: Perform a merge to generate hint files
+if err := db.Merge(); err != nil {
+ log.Fatal(err)
+}
+
+// Step 4: Restart database - hint files will be used for fast startup
+db.Close()
+db, err = nutsdb.Open(options)
+```
+
+#### Disabling HintFile
+
+```go
+// Simply set EnableHintFile to false
+options := nutsdb.DefaultOptions
+options.EnableHintFile = false
+options.EnableMergeV2 = true // Keep Merge V2 if desired
+
+// Hint files will be ignored but not automatically deleted
+// You can manually delete .hint files if you want to reclaim space
+```
+
+### Troubleshooting
+
+#### Common Issues
+
+1. **HintFile Corruption**
+ ```
+ Symptom: Database startup fails or takes long time
+ Solution: NutsDB automatically falls back to scanning data files
+ ```
+
+2. **Merge V2 Memory Usage**
+ ```
+ Symptom: High memory usage during merge operations
+ Solution: Reduce SegmentSize or disable HintFile temporarily
+ ```
+
+3. **Slow Startup with Large Dataset**
+ ```
+ Symptom: Database takes minutes to start
+ Solution: Enable HintFile and perform a manual merge
+ ```
+
+#### Recovery Procedures
+
+```go
+// Check database health
+db, err := nutsdb.Open(options)
+if err != nil {
+ // Try disabling HintFile if corruption suspected
+ options.EnableHintFile = false
+ db, err = nutsdb.Open(options)
+}
+
+// Force rebuild hint files
+if err := db.Merge(); err != nil {
+ log.Printf("Merge failed: %v", err)
+}
+```
+
+### Compatibility
+
+- **Backward Compatible**: Works with existing NutsDB databases
+- **Configurable**: HintFile can be disabled if not needed
+- **Data Structures**: Supports all NutsDB data structures (BTree, Set, List, SortedSet)
+- **Graceful Degradation**: Automatic fallback when hint files are unavailable
+
## Documentation
@@ -112,8 +332,10 @@ func main() {
- More Operation
+ Advanced Features
+- [Merge V2 and HintFile](#merge-v2-and-hintfile)
+- [Watch Key Changes](./docs/user_guides/others.md#watch-key-changes)
- [More Operation](./docs/user_guides/others.md)
diff --git a/vendor/github.com/nutsdb/nutsdb/batch.go b/vendor/github.com/nutsdb/nutsdb/batch.go
index 50e3459c7c..76b656ad69 100644
--- a/vendor/github.com/nutsdb/nutsdb/batch.go
+++ b/vendor/github.com/nutsdb/nutsdb/batch.go
@@ -8,7 +8,7 @@ import (
)
// ErrCommitAfterFinish indicates that write batch commit was called after
-var ErrCommitAfterFinish = errors.New("Batch commit not permitted after finish")
+var ErrCommitAfterFinish = errors.New("batch commit not permitted after finish")
const (
DefaultThrottleSize = 16
diff --git a/vendor/github.com/nutsdb/nutsdb/bucket.go b/vendor/github.com/nutsdb/nutsdb/bucket.go
index 1e20720bf1..060f6b37a6 100644
--- a/vendor/github.com/nutsdb/nutsdb/bucket.go
+++ b/vendor/github.com/nutsdb/nutsdb/bucket.go
@@ -4,6 +4,8 @@ import (
"encoding/binary"
"errors"
"hash/crc32"
+
+ "github.com/nutsdb/nutsdb/internal/utils"
)
var BucketMetaSize int64
@@ -24,7 +26,7 @@ const (
var ErrBucketCrcInvalid = errors.New("bucket crc invalid")
func init() {
- BucketMetaSize = GetDiskSizeFromSingleObject(BucketMeta{})
+ BucketMetaSize = utils.GetDiskSizeFromSingleObject(BucketMeta{})
}
// BucketMeta stores the Meta info of a Bucket. E.g. the size of bucket it store in disk.
diff --git a/vendor/github.com/nutsdb/nutsdb/bucket_manager.go b/vendor/github.com/nutsdb/nutsdb/bucket_manager.go
index bbd0c1ef41..bb519e2518 100644
--- a/vendor/github.com/nutsdb/nutsdb/bucket_manager.go
+++ b/vendor/github.com/nutsdb/nutsdb/bucket_manager.go
@@ -5,7 +5,7 @@ import (
"os"
)
-var ErrBucketNotExist = errors.New("bucket not exist")
+var ErrBucketNotExist = errors.New("bucket not found")
const BucketStoreFileName = "bucket.Meta"
@@ -61,6 +61,8 @@ func (bm *BucketManager) SubmitPendingBucketChange(reqs []*bucketSubmitRequest)
if _, exist := bm.BucketIDMarker[req.name]; !exist {
bm.BucketIDMarker[req.name] = map[Ds]BucketId{}
}
+ // recover maxid otherwise new bucket start from 1 again
+ bm.Gen.CompareAndSetMaxId(req.bucket.Id)
switch req.bucket.Meta.Op {
case BucketInsertOperation:
bm.BucketInfoMapper[req.bucket.Id] = req.bucket
@@ -87,6 +89,12 @@ func (g *IDGenerator) GenId() uint64 {
return g.currentMaxId
}
+func (g *IDGenerator) CompareAndSetMaxId(id uint64) {
+ if id > g.currentMaxId {
+ g.currentMaxId = id
+ }
+}
+
func (bm *BucketManager) ExistBucket(ds Ds, name BucketName) bool {
bucket, err := bm.GetBucket(ds, name)
if bucket != nil && err == nil {
diff --git a/vendor/github.com/nutsdb/nutsdb/datafile.go b/vendor/github.com/nutsdb/nutsdb/datafile.go
index 85a667fe7f..56e213a61d 100644
--- a/vendor/github.com/nutsdb/nutsdb/datafile.go
+++ b/vendor/github.com/nutsdb/nutsdb/datafile.go
@@ -16,6 +16,8 @@ package nutsdb
import (
"errors"
+
+ "github.com/nutsdb/nutsdb/internal/fileio"
)
var (
@@ -39,11 +41,11 @@ type DataFile struct {
fileID int64
writeOff int64
ActualSize int64
- rwManager RWManager
+ rwManager fileio.RWManager
}
// NewDataFile will return a new DataFile Object.
-func NewDataFile(path string, rwManager RWManager) *DataFile {
+func NewDataFile(path string, rwManager fileio.RWManager) *DataFile {
dataFile := &DataFile{
path: path,
rwManager: rwManager,
diff --git a/vendor/github.com/nutsdb/nutsdb/db.go b/vendor/github.com/nutsdb/nutsdb/db.go
index 8dc7e7a0fe..04a6671090 100644
--- a/vendor/github.com/nutsdb/nutsdb/db.go
+++ b/vendor/github.com/nutsdb/nutsdb/db.go
@@ -21,7 +21,6 @@ import (
"io"
"log"
"os"
- "path"
"path/filepath"
"runtime"
"sort"
@@ -29,7 +28,11 @@ import (
"sync"
"time"
+ "github.com/bwmarrin/snowflake"
"github.com/gofrs/flock"
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/fileio"
+ "github.com/nutsdb/nutsdb/internal/utils"
"github.com/xujiajun/utils/filesystem"
"github.com/xujiajun/utils/strconv2"
)
@@ -40,6 +43,13 @@ const KvWriteChCapacity = 1000
const FLockName = "nutsdb-flock"
type (
+ // SnowflakeManager manages snowflake node initialization and caching
+ SnowflakeManager struct {
+ node *snowflake.Node
+ once sync.Once
+ nodeNum int64
+ }
+
// DB represents a collection of buckets that persist on disk.
DB struct {
opt Options // the database options
@@ -50,7 +60,7 @@ type (
KeyCount int // total key number ,include expired, deleted, repeated.
closed bool
isMerging bool
- fm *fileManager
+ fm *FileManager
flock *flock.Flock
commitBuffer *bytes.Buffer
mergeStartCh chan struct{}
@@ -60,10 +70,32 @@ type (
tm *ttlManager
RecordCount int64 // current valid record count, exclude deleted, repeated
bm *BucketManager
- hintKeyAndRAMIdxModeLru *LRUCache // lru cache for HintKeyAndRAMIdxMode
+ hintKeyAndRAMIdxModeLru *utils.LRUCache // lru cache for HintKeyAndRAMIdxMode
+ sm *SnowflakeManager
+ wm *watchManager
}
)
+// NewSnowflakeManager creates a new SnowflakeManager with the given node number
+func NewSnowflakeManager(nodeNum int64) *SnowflakeManager {
+ return &SnowflakeManager{
+ nodeNum: nodeNum,
+ }
+}
+
+// GetNode returns the snowflake node, initializing it once.
+// If initialization fails, it will fatal the program.
+func (sm *SnowflakeManager) GetNode() *snowflake.Node {
+ sm.once.Do(func() {
+ var err error
+ sm.node, err = snowflake.NewNode(sm.nodeNum)
+ if err != nil {
+ log.Fatalf("Failed to initialize snowflake node with nodeNum=%d: %v", sm.nodeNum, err)
+ }
+ })
+ return sm.node
+}
+
// open returns a newly initialized DB object.
func open(opt Options) (*DB, error) {
db := &DB{
@@ -71,14 +103,15 @@ func open(opt Options) (*DB, error) {
opt: opt,
KeyCount: 0,
closed: false,
- Index: newIndex(),
- fm: newFileManager(opt.RWMode, opt.MaxFdNumsInCache, opt.CleanFdsCacheThreshold, opt.SegmentSize),
+ Index: newIndexWithOptions(opt),
+ fm: NewFileManager(opt.RWMode, opt.MaxFdNumsInCache, opt.CleanFdsCacheThreshold, opt.SegmentSize),
mergeStartCh: make(chan struct{}),
mergeEndCh: make(chan error),
mergeWorkCloseCh: make(chan struct{}),
writeCh: make(chan *request, KvWriteChCapacity),
tm: newTTLManager(opt.ExpiredDeleteType),
- hintKeyAndRAMIdxModeLru: NewLruCache(opt.HintKeyAndRAMIdxCacheSize),
+ hintKeyAndRAMIdxModeLru: utils.NewLruCache(opt.HintKeyAndRAMIdxCacheSize),
+ sm: NewSnowflakeManager(opt.NodeNum),
}
db.commitBuffer = createNewBufferWithSize(int(db.opt.CommitBufferSize))
@@ -108,10 +141,21 @@ func open(opt Options) (*DB, error) {
return nil, fmt.Errorf("db.rebuildBucketManager err:%s", err)
}
+ if err := db.recoverMergeManifest(); err != nil {
+ return nil, fmt.Errorf("recover merge manifest: %w", err)
+ }
+
if err := db.buildIndexes(); err != nil {
return nil, fmt.Errorf("db.buildIndexes error: %s", err)
}
+ if db.opt.EnableWatch {
+ db.wm = NewWatchManager()
+ go db.wm.startDistributor()
+ } else {
+ db.wm = nil
+ }
+
go db.mergeWorker()
go db.doWrites()
go db.tm.run()
@@ -156,7 +200,7 @@ func (db *DB) Backup(dir string) error {
// BackupTarGZ Backup copy the database to writer.
func (db *DB) BackupTarGZ(w io.Writer) error {
return db.View(func(tx *Tx) error {
- return tarGZCompress(w, db.opt.Dir)
+ return utils.TarGZCompress(w, db.opt.Dir)
})
}
@@ -192,7 +236,7 @@ func (db *DB) release() error {
db.ActiveFile = nil
- err = db.fm.close()
+ err = db.fm.Close()
if err != nil {
return err
@@ -211,8 +255,16 @@ func (db *DB) release() error {
db.fm = nil
+ db.commitBuffer = nil
+
db.tm.close()
+ if db.wm != nil {
+ if err := db.wm.close(); err != nil {
+ log.Printf("watch manager closed already")
+ }
+ }
+
if GCEnable {
runtime.GC()
}
@@ -220,7 +272,7 @@ func (db *DB) release() error {
return nil
}
-func (db *DB) getValueByRecord(record *Record) ([]byte, error) {
+func (db *DB) getValueByRecord(record *data.Record) ([]byte, error) {
if record == nil {
return nil, ErrRecordIsNil
}
@@ -237,11 +289,11 @@ func (db *DB) getValueByRecord(record *Record) ([]byte, error) {
}
dirPath := getDataPath(record.FileID, db.opt.Dir)
- df, err := db.fm.getDataFile(dirPath, db.opt.SegmentSize)
+ df, err := db.fm.GetDataFileReadOnly(dirPath, db.opt.SegmentSize)
if err != nil {
return nil, err
}
- defer func(rwManager RWManager) {
+ defer func(rwManager fileio.RWManager) {
err := rwManager.Release()
if err != nil {
return
@@ -271,7 +323,6 @@ func (db *DB) commitTransaction(tx *Tx) error {
panicked = true
}
if panicked || err != nil {
- // log.Fatal("panicked=", panicked, ", err=", err)
if errRollback := tx.Rollback(); errRollback != nil {
err = errRollback
}
@@ -283,7 +334,6 @@ func (db *DB) commitTransaction(tx *Tx) error {
tx.setStatusRunning()
err = tx.Commit()
if err != nil {
- // log.Fatal("txCommit fail,err=", err)
return err
}
@@ -333,6 +383,11 @@ func (db *DB) getHintKeyAndRAMIdxCacheSize() int {
return db.opt.HintKeyAndRAMIdxCacheSize
}
+// getSnowflakeNode returns a cached snowflake node, creating it once if needed.
+func (db *DB) getSnowflakeNode() *snowflake.Node {
+ return db.sm.GetNode()
+}
+
func (db *DB) doWrites() {
pendingCh := make(chan struct{}, 1)
writeRequests := func(reqs []*request) {
@@ -393,7 +448,7 @@ func (db *DB) doWrites() {
// setActiveFile sets the ActiveFile (DataFile object).
func (db *DB) setActiveFile() (err error) {
activeFilePath := getDataPath(db.MaxFileID, db.opt.Dir)
- db.ActiveFile, err = db.fm.getDataFile(activeFilePath, db.opt.SegmentSize)
+ db.ActiveFile, err = db.fm.GetDataFile(activeFilePath, db.opt.SegmentSize)
if err != nil {
return
}
@@ -404,36 +459,32 @@ func (db *DB) setActiveFile() (err error) {
}
// getMaxFileIDAndFileIds returns max fileId and fileIds.
-func (db *DB) getMaxFileIDAndFileIDs() (maxFileID int64, dataFileIds []int) {
- files, _ := os.ReadDir(db.opt.Dir)
-
- if len(files) == 0 {
+func (db *DB) getMaxFileIDAndFileIDs() (maxFileID int64, dataFileIds []int64) {
+ userIDs, mergeIDs, err := enumerateDataFileIDs(db.opt.Dir)
+ if err != nil {
return 0, nil
}
- for _, file := range files {
- filename := file.Name()
- fileSuffix := path.Ext(path.Base(filename))
- if fileSuffix != DataSuffix {
- continue
- }
+ if len(userIDs) > 0 {
+ maxFileID = userIDs[len(userIDs)-1]
+ }
+
+ dataFileIds = make([]int64, 0, len(userIDs)+len(mergeIDs))
+ dataFileIds = append(dataFileIds, userIDs...)
+ dataFileIds = append(dataFileIds, mergeIDs...)
- filename = strings.TrimSuffix(filename, DataSuffix)
- id, _ := strconv2.StrToInt(filename)
- dataFileIds = append(dataFileIds, id)
+ if len(dataFileIds) > 1 {
+ sort.Slice(dataFileIds, func(i, j int) bool { return dataFileIds[i] < dataFileIds[j] })
}
if len(dataFileIds) == 0 {
- return 0, nil
+ return maxFileID, nil
}
- sort.Ints(dataFileIds)
- maxFileID = int64(dataFileIds[len(dataFileIds)-1])
-
- return
+ return maxFileID, dataFileIds
}
-func (db *DB) parseDataFiles(dataFileIds []int) (err error) {
+func (db *DB) parseDataFiles(dataFileIds []int64) (err error) {
var (
off int64
f *fileRecovery
@@ -523,13 +574,25 @@ func (db *DB) parseDataFiles(dataFileIds []int) (err error) {
for _, dataID := range dataFileIds {
off = 0
- fID = int64(dataID)
+ fID = dataID
+
+ // Try to load hint file first if enabled
+ if db.opt.EnableHintFile {
+ hintLoaded, _ := db.loadHintFile(fID)
+
+ if hintLoaded {
+ // Hint file loaded successfully, skip scanning data file
+ continue
+ }
+ }
+
+ // Fall back to scanning data file
dataPath := getDataPath(fID, db.opt.Dir)
f, err = newFileRecovery(dataPath, db.opt.BufferSizeOfRecovery)
if err != nil {
return err
}
- err := readEntriesFromFile()
+ err = readEntriesFromFile()
if err != nil {
return err
}
@@ -540,6 +603,108 @@ func (db *DB) parseDataFiles(dataFileIds []int) (err error) {
return
}
+// loadHintFile loads a single hint file and rebuilds indexes
+func (db *DB) loadHintFile(fid int64) (bool, error) {
+ hintPath := getHintPath(fid, db.opt.Dir)
+
+ // Check if hint file exists
+ if _, err := os.Stat(hintPath); os.IsNotExist(err) {
+ return false, nil // Hint file doesn't exist, need to scan data file
+ }
+
+ reader := &HintFileReader{}
+ if err := reader.Open(hintPath); err != nil {
+ return false, nil
+ }
+ defer func() {
+ if err := reader.Close(); err != nil {
+ // Log error but don't fail the operation
+ log.Printf("Warning: failed to close hint file reader: %v", err)
+ }
+ }()
+
+ // Read all hint entries and build indexes
+ for {
+ hintEntry, err := reader.Read()
+ if err != nil {
+ if err == io.EOF {
+ break // End of file
+ }
+ return false, nil
+ }
+
+ if hintEntry == nil {
+ continue
+ }
+
+ // Check if bucket exists
+ bucketId := hintEntry.BucketId
+ if _, err := db.bm.GetBucketById(bucketId); errors.Is(err, ErrBucketNotExist) {
+ continue // Skip if bucket doesn't exist
+ }
+
+ // Create a record from hint entry
+ record := data.NewRecord()
+ record.WithKey(hintEntry.Key).
+ WithFileId(hintEntry.FileID).
+ WithDataPos(hintEntry.DataPos).
+ WithValueSize(hintEntry.ValueSize).
+ WithTimestamp(hintEntry.Timestamp).
+ WithTTL(hintEntry.TTL).
+ WithTxID(0) // TxID is not stored in hint file
+
+ // Create an entry from hint entry
+ entry := NewEntry()
+ entry.WithKey(hintEntry.Key)
+
+ // Create metadata
+ meta := NewMetaData()
+ meta.WithBucketId(hintEntry.BucketId).
+ WithKeySize(hintEntry.KeySize).
+ WithValueSize(hintEntry.ValueSize).
+ WithTimeStamp(hintEntry.Timestamp).
+ WithTTL(hintEntry.TTL).
+ WithFlag(hintEntry.Flag).
+ WithStatus(hintEntry.Status).
+ WithDs(hintEntry.Ds).
+ WithTxID(0) // TxID is not stored in hint file
+
+ entry.WithMeta(meta)
+
+ // In HintKeyValAndRAMIdxMode, we need to load the value from data file
+ if db.opt.EntryIdxMode == HintKeyValAndRAMIdxMode {
+ value, err := db.getValueByRecord(record)
+ if err != nil {
+ // If we can't load the value, we can't use this entry in HintKeyValAndRAMIdxMode
+ // Skip this entry and continue
+ continue
+ }
+ entry.WithValue(value)
+ record.WithValue(value)
+ } else if db.opt.EntryIdxMode == HintKeyAndRAMIdxMode {
+ // In HintKeyAndRAMIdxMode, for Set data structure, we also need to load the value
+ // because Set uses value hash as the key in its internal map structure
+ if hintEntry.Ds == DataStructureSet || hintEntry.Ds == DataStructureSortedSet {
+ value, err := db.getValueByRecord(record)
+ if err != nil {
+ continue
+ }
+ entry.WithValue(value)
+ // Don't set record.WithValue for HintKeyAndRAMIdxMode, as we only need it for index building
+ }
+ }
+
+ // Build indexes
+ if err := db.buildIdxes(record, entry); err != nil {
+ continue
+ }
+
+ db.KeyCount++
+ }
+
+ return true, nil
+}
+
func (db *DB) getRecordCount() (int64, error) {
var res int64
@@ -580,7 +745,7 @@ func (db *DB) getRecordCount() (int64, error) {
return res, nil
}
-func (db *DB) buildBTreeIdx(record *Record, entry *Entry) error {
+func (db *DB) buildBTreeIdx(record *data.Record, entry *Entry) error {
key, meta := entry.Key, entry.Meta
bucket, err := db.bm.GetBucketById(meta.BucketId)
@@ -596,23 +761,16 @@ func (db *DB) buildBTreeIdx(record *Record, entry *Entry) error {
bTree.Delete(key)
} else {
if meta.TTL != Persistent {
- db.tm.add(bucketId, string(key), db.expireTime(meta.Timestamp, meta.TTL), db.buildExpireCallback(bucket.Name, key))
+ db.tm.add(bucketId, string(key), expireTime(meta.Timestamp, meta.TTL), db.buildExpireCallback(bucket.Name, key))
} else {
db.tm.del(bucketId, string(key))
}
- bTree.Insert(record)
+ bTree.InsertRecord(record.Key, record)
}
return nil
}
-func (db *DB) expireTime(timestamp uint64, ttl uint32) time.Duration {
- now := time.UnixMilli(time.Now().UnixMilli())
- expireTime := time.UnixMilli(int64(timestamp))
- expireTime = expireTime.Add(time.Duration(int64(ttl)) * time.Second)
- return expireTime.Sub(now)
-}
-
-func (db *DB) buildIdxes(record *Record, entry *Entry) error {
+func (db *DB) buildIdxes(record *data.Record, entry *Entry) error {
meta := entry.Meta
switch meta.Ds {
case DataStructureBTree:
@@ -635,23 +793,8 @@ func (db *DB) buildIdxes(record *Record, entry *Entry) error {
return nil
}
-func (db *DB) deleteBucket(ds uint16, bucket BucketId) {
- if ds == DataStructureSet {
- db.Index.set.delete(bucket)
- }
- if ds == DataStructureSortedSet {
- db.Index.sortedSet.delete(bucket)
- }
- if ds == DataStructureBTree {
- db.Index.bTree.delete(bucket)
- }
- if ds == DataStructureList {
- db.Index.list.delete(bucket)
- }
-}
-
// buildSetIdx builds set index when opening the DB.
-func (db *DB) buildSetIdx(record *Record, entry *Entry) error {
+func (db *DB) buildSetIdx(record *data.Record, entry *Entry) error {
key, val, meta := entry.Key, entry.Value, entry.Meta
bucket, err := db.bm.GetBucketById(entry.Meta.BucketId)
@@ -664,7 +807,7 @@ func (db *DB) buildSetIdx(record *Record, entry *Entry) error {
switch meta.Flag {
case DataSetFlag:
- if err := s.SAdd(string(key), [][]byte{val}, []*Record{record}); err != nil {
+ if err := s.SAdd(string(key), [][]byte{val}, []*data.Record{record}); err != nil {
return fmt.Errorf("when build SetIdx SAdd index err: %s", err)
}
case DataDeleteFlag:
@@ -677,7 +820,7 @@ func (db *DB) buildSetIdx(record *Record, entry *Entry) error {
}
// buildSortedSetIdx builds sorted set index when opening the DB.
-func (db *DB) buildSortedSetIdx(record *Record, entry *Entry) error {
+func (db *DB) buildSortedSetIdx(record *data.Record, entry *Entry) error {
key, val, meta := entry.Key, entry.Value, entry.Meta
bucket, err := db.bm.GetBucketById(entry.Meta.BucketId)
@@ -716,7 +859,7 @@ func (db *DB) buildSortedSetIdx(record *Record, entry *Entry) error {
}
// buildListIdx builds List index when opening the DB.
-func (db *DB) buildListIdx(record *Record, entry *Entry) error {
+func (db *DB) buildListIdx(record *data.Record, entry *Entry) error {
key, val, meta := entry.Key, entry.Value, entry.Meta
bucket, err := db.bm.GetBucketById(entry.Meta.BucketId)
@@ -727,7 +870,7 @@ func (db *DB) buildListIdx(record *Record, entry *Entry) error {
l := db.Index.list.getWithDefault(bucketId)
- if IsExpired(meta.TTL, meta.Timestamp) {
+ if data.IsExpired(meta.TTL, meta.Timestamp) {
return nil
}
@@ -752,7 +895,7 @@ func (db *DB) buildListIdx(record *Record, entry *Entry) error {
end, _ := strconv2.StrToInt(string(val))
err = l.LTrim(newKey, start, end)
case DataLRemByIndex:
- indexes, _ := UnmarshalInts(val)
+ indexes, _ := utils.UnmarshalInts(val)
err = l.LRemByIndex(string(key), indexes)
}
@@ -763,10 +906,10 @@ func (db *DB) buildListIdx(record *Record, entry *Entry) error {
return nil
}
-func (db *DB) buildListLRemIdx(value []byte, l *List, key []byte) error {
+func (db *DB) buildListLRemIdx(value []byte, l *data.List, key []byte) error {
count, newValue := splitIntStringStr(string(value), SeparatorForListKey)
- return l.LRem(string(key), count, func(r *Record) (bool, error) {
+ return l.LRem(string(key), count, func(r *data.Record) (bool, error) {
v, err := db.getValueByRecord(r)
if err != nil {
return false, err
@@ -779,7 +922,7 @@ func (db *DB) buildListLRemIdx(value []byte, l *List, key []byte) error {
func (db *DB) buildIndexes() (err error) {
var (
maxFileID int64
- dataFileIds []int
+ dataFileIds []int64
)
maxFileID, dataFileIds = db.getMaxFileIDAndFileIDs()
@@ -792,7 +935,7 @@ func (db *DB) buildIndexes() (err error) {
return
}
- if dataFileIds == nil && maxFileID == 0 {
+ if len(dataFileIds) == 0 {
return
}
@@ -800,8 +943,8 @@ func (db *DB) buildIndexes() (err error) {
return db.parseDataFiles(dataFileIds)
}
-func (db *DB) createRecordByModeWithFidAndOff(fid int64, off uint64, entry *Entry) *Record {
- record := NewRecord()
+func (db *DB) createRecordByModeWithFidAndOff(fid int64, off uint64, entry *Entry) *data.Record {
+ record := data.NewRecord()
record.WithKey(entry.Key).
WithTimestamp(entry.Meta.Timestamp).
@@ -861,7 +1004,7 @@ func (db *DB) sendToWriteCh(tx *Tx) (*request, error) {
}
func (db *DB) checkListExpired() {
- db.Index.list.rangeIdx(func(l *List) {
+ db.Index.list.rangeIdx(func(l *data.List) {
for key := range l.TTL {
l.IsExpire(key)
}
@@ -875,7 +1018,7 @@ func (db *DB) IsClose() bool {
func (db *DB) buildExpireCallback(bucket string, key []byte) func() {
return func() {
- err := db.Update(func(tx *Tx) error {
+ _ = db.Update(func(tx *Tx) error {
b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
if err != nil {
return err
@@ -886,9 +1029,6 @@ func (db *DB) buildExpireCallback(bucket string, key []byte) func() {
}
return nil
})
- if err != nil {
- log.Printf("occur error when expired deletion, error: %v", err.Error())
- }
}
}
@@ -926,3 +1066,108 @@ func (db *DB) rebuildBucketManager() error {
}
return nil
}
+
+/**
+ * Watch watches the key and bucket and calls the callback function for each message received.
+ * The callback will be called once for each individual message in the batch.
+ *
+ * @param bucket - the bucket name to watch
+ * @param key - the key in the bucket to watch
+ * @param cb - the callback function to call for each message received
+ * @param opts - the options for the watch
+ * - CallbackTimeout - the timeout for the callback, default is 1 second
+ *
+ * @return error - the error if the watch is stopped
+ */
+func (db *DB) Watch(bucket string, key []byte, cb func(message *Message) error, opts ...WatchOptions) error {
+ watchOpts := NewWatchOptions()
+
+ if len(opts) > 0 {
+ watchOpts = &opts[0]
+ }
+
+ if db.wm == nil {
+ return ErrWatchFeatureDisabled
+ }
+
+ subscriber, err := db.wm.subscribe(bucket, string(key))
+ if err != nil {
+ return err
+ }
+
+ maxBatchSize := 128
+ batch := make([]*Message, 0, maxBatchSize)
+
+ // Use a ticker to process the batch every 100 milliseconds
+ // Avoid CPU busy spinning
+ ticker := time.NewTicker(100 * time.Millisecond)
+ keyWatch := string(key)
+
+ processBatch := func(batch []*Message) error {
+ if len(batch) == 0 {
+ return nil
+ }
+
+ for _, msg := range batch {
+ errChan := make(chan error, 1)
+ go func(msg *Message) {
+ errChan <- cb(msg)
+ }(msg)
+
+ select {
+ case <-time.After(watchOpts.CallbackTimeout):
+ return ErrWatchingCallbackTimeout
+ case err := <-errChan:
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ }
+
+ defer func() {
+ ticker.Stop()
+
+ if subscriber != nil && subscriber.active.Load() {
+ if err := db.wm.unsubscribe(bucket, keyWatch, subscriber.id); err != nil {
+ // ignore the error
+ }
+ }
+ }()
+
+ for {
+ select {
+ case <-db.wm.done():
+ // drain the batch
+ if err := processBatch(batch); err != nil {
+ return err
+ }
+ return nil
+ case message, ok := <-subscriber.receiveChan:
+ if !ok {
+ if err := processBatch(batch); err != nil {
+ return err
+ }
+
+ return nil
+ }
+
+ batch = append(batch, message)
+
+ if len(batch) >= maxBatchSize {
+ if err := processBatch(batch); err != nil {
+ return err
+ }
+ batch = batch[:0]
+ }
+ case <-ticker.C:
+ for len(batch) > 0 {
+ if err := processBatch(batch); err != nil {
+ return err
+ }
+ batch = batch[:0]
+ }
+ }
+ }
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/db_error.go b/vendor/github.com/nutsdb/nutsdb/db_error.go
index df01883db3..9225a6e843 100644
--- a/vendor/github.com/nutsdb/nutsdb/db_error.go
+++ b/vendor/github.com/nutsdb/nutsdb/db_error.go
@@ -32,4 +32,10 @@ var (
// ErrRecordIsNil is returned when Record is nil
ErrRecordIsNil = errors.New("the record is nil")
+
+ // ErrWatchFeatureDisabled is returned when the watch feature is disabled
+ ErrWatchFeatureDisabled = errors.New("watch feature is disabled")
+
+ // ErrWatchingCallbackTimeout is returned when the callback timeout
+ ErrWatchingCallbackTimeout = errors.New("watching callback timeout")
)
diff --git a/vendor/github.com/nutsdb/nutsdb/entity_utils.go b/vendor/github.com/nutsdb/nutsdb/entity_utils.go
deleted file mode 100644
index 3ec5753fb3..0000000000
--- a/vendor/github.com/nutsdb/nutsdb/entity_utils.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package nutsdb
-
-import "reflect"
-
-func GetDiskSizeFromSingleObject(obj interface{}) int64 {
- typ := reflect.TypeOf(obj)
- fields := reflect.VisibleFields(typ)
- if len(fields) == 0 {
- return 0
- }
- var size int64 = 0
- for _, field := range fields {
- // Currently, we only use the unsigned value type for our metadata.go. That's reasonable for us.
- // Because it's not possible to use negative value mark the size of data.
- // But if you want to make it more flexible, please help yourself.
- switch field.Type.Kind() {
- case reflect.Uint8:
- size += 1
- case reflect.Uint16:
- size += 2
- case reflect.Uint32:
- size += 4
- case reflect.Uint64:
- size += 8
- }
- }
- return size
-}
diff --git a/vendor/github.com/nutsdb/nutsdb/entry.go b/vendor/github.com/nutsdb/nutsdb/entry.go
index 3610a31b56..fd9cdd2509 100644
--- a/vendor/github.com/nutsdb/nutsdb/entry.go
+++ b/vendor/github.com/nutsdb/nutsdb/entry.go
@@ -15,12 +15,15 @@
package nutsdb
import (
+ "bytes"
"encoding/binary"
"errors"
"hash/crc32"
"sort"
"strings"
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/utils"
"github.com/xujiajun/utils/strconv2"
)
@@ -196,7 +199,7 @@ func (e *Entry) isFilter() bool {
DataZPopMinFlag,
DataLRemByIndex,
}
- return OneOfUint16Array(meta.Flag, filterDataSet)
+ return utils.OneOfUint16Array(meta.Flag, filterDataSet)
}
// valid check the entry fields valid or not
@@ -238,8 +241,15 @@ func (e *Entry) GetTxIDBytes() []byte {
return []byte(strconv2.Int64ToStr(int64(e.Meta.TxID)))
}
+func (e *Entry) IsBelongsToBTree() bool {
+ return e.Meta.IsBTree()
+}
+
+// IsBelongsToBPlusTree is kept for backward compatibility with legacy naming.
+// Internally nutsdb uses a B+ tree implementation for primary indexes, so both
+// helpers map to the same metadata flag.
func (e *Entry) IsBelongsToBPlusTree() bool {
- return e.Meta.IsBPlusTree()
+ return e.IsBelongsToBTree()
}
func (e *Entry) IsBelongsToList() bool {
@@ -272,7 +282,7 @@ func (e Entries) processEntriesScanOnDisk() (result []*Entry) {
sort.Sort(e)
for _, ele := range e {
curE := ele
- if !IsExpired(curE.Meta.TTL, curE.Meta.Timestamp) && curE.Meta.Flag != DataDeleteFlag {
+ if !data.IsExpired(curE.Meta.TTL, curE.Meta.Timestamp) && curE.Meta.Flag != DataDeleteFlag {
result = append(result, curE)
}
}
@@ -310,7 +320,7 @@ func (c CEntries) processEntriesScanOnDisk() (result []*Entry) {
sort.Sort(c)
for _, ele := range c.Entries {
curE := ele
- if !IsExpired(curE.Meta.TTL, curE.Meta.Timestamp) && curE.Meta.Flag != DataDeleteFlag {
+ if !data.IsExpired(curE.Meta.TTL, curE.Meta.Timestamp) && curE.Meta.Flag != DataDeleteFlag {
result = append(result, curE)
}
}
@@ -342,3 +352,45 @@ func (dt *dataInTx) reset() {
dt.es = make([]*EntryWhenRecovery, 0)
dt.txId = 0
}
+
+/**
+ * decode the key of the entry
+ * 1. in the case of list flag is DataLPushFlag or DataRPushFlag, the key is transformed from seq + user_key to user_key
+ * so we need to decode the key to get the raw key
+ * 2. in the case of sorted set flag is DataZAddFlag, the key is transformed from score + user_key to user_key
+ * so we need to decode the key to get the raw key
+ * 3. All other cases, the key is the raw key
+ */
+func (entry *Entry) getRawKey() ([]byte, error) {
+ key := entry.Key
+
+ switch entry.Meta.Ds {
+ case DataStructureList:
+ if entry.Meta.Flag != DataLPushFlag && entry.Meta.Flag != DataRPushFlag {
+ return key, nil
+ }
+
+ if len(key) < 8 {
+ return key, ErrInvalidKey
+ }
+
+ return key[8:], nil
+ case DataStructureSortedSet:
+ if entry.Meta.Flag != DataZAddFlag {
+ return key, nil
+ }
+
+ strList := bytes.Split(key, []byte(SeparatorForZSetKey))
+ if len(strList) != 2 {
+ return key, ErrInvalidKey
+ }
+
+ return []byte(strList[0]), nil
+ case DataStructureSet:
+ return key, nil
+ case DataStructureBTree:
+ return key, nil
+ default:
+ return key, ErrDataStructureNotSupported
+ }
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/errors.go b/vendor/github.com/nutsdb/nutsdb/errors.go
index 18a82f071a..716a03ce9d 100644
--- a/vendor/github.com/nutsdb/nutsdb/errors.go
+++ b/vendor/github.com/nutsdb/nutsdb/errors.go
@@ -2,6 +2,8 @@ package nutsdb
import (
"errors"
+
+ "github.com/nutsdb/nutsdb/internal/fileio"
)
// IsDBClosed is true if the error indicates the db was closed.
@@ -11,7 +13,7 @@ func IsDBClosed(err error) bool {
// IsKeyNotFound is true if the error indicates the key is not found.
func IsKeyNotFound(err error) bool {
- return errors.Is(err, ErrKeyNotFound)
+ return errors.Is(err, ErrNotFoundKey)
}
// IsBucketNotFound is true if the error indicates the bucket is not exists.
@@ -38,3 +40,7 @@ func IsPrefixScan(err error) bool {
func IsPrefixSearchScan(err error) bool {
return errors.Is(err, ErrPrefixSearchScan)
}
+
+var (
+ ErrIndexOutOfBound = fileio.ErrIndexOutOfBound
+)
diff --git a/vendor/github.com/nutsdb/nutsdb/file_manager.go b/vendor/github.com/nutsdb/nutsdb/file_manager.go
index a1678e1d57..b6a968a6a0 100644
--- a/vendor/github.com/nutsdb/nutsdb/file_manager.go
+++ b/vendor/github.com/nutsdb/nutsdb/file_manager.go
@@ -1,43 +1,63 @@
package nutsdb
-import (
- "github.com/xujiajun/mmap-go"
+import "github.com/nutsdb/nutsdb/internal/fileio"
+
+// RWMode represents the read and write mode.
+type RWMode int
+
+const (
+ // FileIO represents the read and write mode using standard I/O.
+ FileIO RWMode = iota
+
+ // MMap represents the read and write mode using mmap.
+ MMap
)
-// fileManager holds the fd cache and file-related operations go through the manager to obtain the file processing object
-type fileManager struct {
+// FileManager holds the fd cache and file-related operations go through the manager to obtain the file processing object
+type FileManager struct {
rwMode RWMode
- fdm *fdManager
+ fdm *fileio.FdManager
segmentSize int64
}
-// newFileManager will create a newFileManager object
-func newFileManager(rwMode RWMode, maxFdNums int, cleanThreshold float64, segmentSize int64) (fm *fileManager) {
- fm = &fileManager{
+// NewFileManager will create a NewFileManager object
+func NewFileManager(rwMode RWMode, maxFdNums int, cleanThreshold float64, segmentSize int64) (fm *FileManager) {
+ fm = &FileManager{
rwMode: rwMode,
- fdm: newFdm(maxFdNums, cleanThreshold),
+ fdm: fileio.NewFdm(maxFdNums, cleanThreshold),
segmentSize: segmentSize,
}
return fm
}
-// getDataFile will return a DataFile Object
-func (fm *fileManager) getDataFile(path string, capacity int64) (datafile *DataFile, err error) {
+// GetDataFile will return a DataFile Object
+func (fm *FileManager) GetDataFile(path string, capacity int64) (datafile *DataFile, err error) {
+ return fm.getDataFileWithMode(path, capacity, false)
+}
+
+// GetDataFileReadOnly will return a DataFile Object for read-only operations
+// This method skips file truncation to improve read performance
+func (fm *FileManager) GetDataFileReadOnly(path string, capacity int64) (datafile *DataFile, err error) {
+ return fm.getDataFileWithMode(path, capacity, true)
+}
+
+// getDataFileWithMode will return a DataFile Object with specified read-only mode
+func (fm *FileManager) getDataFileWithMode(path string, capacity int64, readOnly bool) (datafile *DataFile, err error) {
if capacity <= 0 {
return nil, ErrCapacity
}
- var rwManager RWManager
+ var rwManager fileio.RWManager
if fm.rwMode == FileIO {
- rwManager, err = fm.getFileRWManager(path, capacity, fm.segmentSize)
+ rwManager, err = fm.GetFileRWManager(path, capacity, fm.segmentSize, readOnly)
if err != nil {
return nil, err
}
}
if fm.rwMode == MMap {
- rwManager, err = fm.getMMapRWManager(path, capacity, fm.segmentSize)
+ rwManager, err = fm.GetMMapRWManager(path, capacity, fm.segmentSize, readOnly)
if err != nil {
return nil, err
}
@@ -46,42 +66,42 @@ func (fm *fileManager) getDataFile(path string, capacity int64) (datafile *DataF
return NewDataFile(path, rwManager), nil
}
-// getFileRWManager will return a FileIORWManager Object
-func (fm *fileManager) getFileRWManager(path string, capacity int64, segmentSize int64) (*FileIORWManager, error) {
- fd, err := fm.fdm.getFd(path)
+func (fm *FileManager) GetDataFileByID(dir string, fileID int64, capacity int64) (*DataFile, error) {
+ path := getDataPath(fileID, dir)
+ return fm.GetDataFile(path, capacity)
+}
+
+// GetFileRWManager will return a FileIORWManager Object
+func (fm *FileManager) GetFileRWManager(path string, capacity int64, segmentSize int64, readOnly bool) (*fileio.FileIORWManager, error) {
+ fd, err := fm.fdm.GetFd(path)
if err != nil {
return nil, err
}
- err = Truncate(path, capacity, fd)
+ err = fileio.Truncate(path, capacity, fd, readOnly)
if err != nil {
return nil, err
}
- return &FileIORWManager{fd: fd, path: path, fdm: fm.fdm, segmentSize: segmentSize}, nil
+ return &fileio.FileIORWManager{Fd: fd, Path: path, Fdm: fm.fdm, SegmentSize: segmentSize}, nil
}
-// getMMapRWManager will return a MMapRWManager Object
-func (fm *fileManager) getMMapRWManager(path string, capacity int64, segmentSize int64) (*MMapRWManager, error) {
- fd, err := fm.fdm.getFd(path)
- if err != nil {
- return nil, err
- }
-
- err = Truncate(path, capacity, fd)
+// GetMMapRWManager will return a MMapRWManager Object
+func (fm *FileManager) GetMMapRWManager(path string, capacity int64, segmentSize int64, readOnly bool) (*fileio.MMapRWManager, error) {
+ fd, err := fm.fdm.GetFd(path)
if err != nil {
return nil, err
}
- m, err := mmap.Map(fd, mmap.RDWR, 0)
+ err = fileio.Truncate(path, capacity, fd, readOnly)
if err != nil {
return nil, err
}
- return &MMapRWManager{m: m, path: path, fdm: fm.fdm, segmentSize: segmentSize}, nil
+ return fileio.GetMMapRWManager(fd, path, fm.fdm, segmentSize), nil
}
-// close will close fdm resource
-func (fm *fileManager) close() error {
- err := fm.fdm.close()
+// Close will Close fdm resource
+func (fm *FileManager) Close() error {
+ err := fm.fdm.Close()
return err
}
diff --git a/vendor/github.com/nutsdb/nutsdb/hint_collector.go b/vendor/github.com/nutsdb/nutsdb/hint_collector.go
new file mode 100644
index 0000000000..7727a01f5a
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/hint_collector.go
@@ -0,0 +1,100 @@
+package nutsdb
+
+import "errors"
+
+var errHintCollectorClosed = errors.New("hint collector closed")
+
+const DefaultHintCollectorFlushEvery = 1024
+
+type hintWriter interface {
+ Write(*HintEntry) error
+ Sync() error
+ Close() error
+}
+
+type HintCollector struct {
+ writer hintWriter
+ buf []HintEntry
+ fileID int64
+ flushEvery int
+ closed bool
+}
+
+func NewHintCollector(fileID int64, writer hintWriter, flushEvery int) *HintCollector {
+ if flushEvery <= 0 {
+ flushEvery = DefaultHintCollectorFlushEvery
+ }
+ return &HintCollector{
+ writer: writer,
+ buf: make([]HintEntry, 0, flushEvery),
+ fileID: fileID,
+ flushEvery: flushEvery,
+ }
+}
+
+func (hc *HintCollector) Add(entry *HintEntry) error {
+ if hc.closed {
+ return errHintCollectorClosed
+ }
+ if entry == nil {
+ return ErrHintFileEntryInvalid
+ }
+ clone := *entry
+ clone.FileID = hc.fileID
+ if len(entry.Key) > 0 {
+ clone.Key = append([]byte(nil), entry.Key...)
+ }
+ hc.buf = append(hc.buf, clone)
+ if len(hc.buf) >= hc.flushEvery {
+ return hc.flush(true)
+ }
+ return nil
+}
+
+func (hc *HintCollector) Flush() error {
+ if hc.closed {
+ return errHintCollectorClosed
+ }
+ return hc.flush(true)
+}
+
+func (hc *HintCollector) Sync() error {
+ if hc.closed {
+ return errHintCollectorClosed
+ }
+ if err := hc.flush(false); err != nil {
+ return err
+ }
+ return hc.writer.Sync()
+}
+
+func (hc *HintCollector) Close() error {
+ if hc.closed {
+ return nil
+ }
+ if err := hc.flush(true); err != nil {
+ return err
+ }
+ hc.closed = true
+ return hc.writer.Close()
+}
+
+func (hc *HintCollector) flush(sync bool) error {
+ if len(hc.buf) == 0 {
+ if sync {
+ return hc.writer.Sync()
+ }
+ return nil
+ }
+ for i := range hc.buf {
+ entry := hc.buf[i]
+ if err := hc.writer.Write(&entry); err != nil {
+ return err
+ }
+ }
+ hc.buf = hc.buf[:0]
+ if sync {
+ return hc.writer.Sync()
+ }
+ return nil
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/hintfile.go b/vendor/github.com/nutsdb/nutsdb/hintfile.go
new file mode 100644
index 0000000000..0efd5551a3
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/hintfile.go
@@ -0,0 +1,472 @@
+// Copyright 2019 The nutsdb Author. All rights reserved.
+//
+// Licensed 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 nutsdb
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/nutsdb/nutsdb/internal/utils"
+ "github.com/xujiajun/utils/strconv2"
+)
+
+const (
+ // HintSuffix returns the hint file suffix
+ HintSuffix = ".hint"
+)
+
+var (
+ // ErrHintFileCorrupted is returned when hint file is corrupted
+ ErrHintFileCorrupted = errors.New("hint file is corrupted")
+ // ErrHintFileEntryInvalid is returned when hint entry is invalid
+ ErrHintFileEntryInvalid = errors.New("hint file entry is invalid")
+)
+
+// getHintPath returns the hint file path for the given file ID and directory
+func getHintPath(fid int64, dir string) string {
+ separator := string(filepath.Separator)
+ if IsMergeFile(fid) {
+ seq := GetMergeSeq(fid)
+ return dir + separator + fmt.Sprintf("merge_%d%s", seq, HintSuffix)
+ }
+ return dir + separator + strconv2.Int64ToStr(fid) + HintSuffix
+}
+
+// HintEntry represents an entry in the hint file
+type HintEntry struct {
+ BucketId uint64
+ KeySize uint32
+ ValueSize uint32
+ Timestamp uint64
+ TTL uint32
+ Flag uint16
+ Status uint16
+ Ds uint16
+ DataPos uint64
+ FileID int64
+ Key []byte
+}
+
+func newHintEntryFromEntry(entry *Entry, fileID int64, offset uint64) *HintEntry {
+ meta := entry.Meta
+ return &HintEntry{
+ BucketId: meta.BucketId,
+ KeySize: meta.KeySize,
+ ValueSize: meta.ValueSize,
+ Timestamp: meta.Timestamp,
+ TTL: meta.TTL,
+ Flag: meta.Flag,
+ Status: meta.Status,
+ Ds: meta.Ds,
+ DataPos: offset,
+ FileID: fileID,
+ Key: append([]byte(nil), entry.Key...),
+ }
+}
+
+// Size returns the size of the hint entry
+func (h *HintEntry) Size() int64 {
+ keySize := len(h.Key)
+
+ size := 0
+ size += utils.UvarintSize(h.BucketId)
+ size += utils.UvarintSize(uint64(keySize))
+ size += utils.UvarintSize(uint64(h.ValueSize))
+ size += utils.UvarintSize(h.Timestamp)
+ size += utils.UvarintSize(uint64(h.TTL))
+ size += utils.UvarintSize(uint64(h.Flag))
+ size += utils.UvarintSize(uint64(h.Status))
+ size += utils.UvarintSize(uint64(h.Ds))
+ size += utils.UvarintSize(h.DataPos)
+ size += utils.VarintSize(h.FileID)
+ size += keySize
+
+ return int64(size)
+}
+
+// Encode encodes the hint entry to bytes
+func (h *HintEntry) Encode() []byte {
+ keySize := len(h.Key)
+ h.KeySize = uint32(keySize)
+
+ buf := make([]byte, h.Size())
+ index := 0
+
+ index += binary.PutUvarint(buf[index:], h.BucketId)
+ index += binary.PutUvarint(buf[index:], uint64(keySize))
+ index += binary.PutUvarint(buf[index:], uint64(h.ValueSize))
+ index += binary.PutUvarint(buf[index:], h.Timestamp)
+ index += binary.PutUvarint(buf[index:], uint64(h.TTL))
+ index += binary.PutUvarint(buf[index:], uint64(h.Flag))
+ index += binary.PutUvarint(buf[index:], uint64(h.Status))
+ index += binary.PutUvarint(buf[index:], uint64(h.Ds))
+ index += binary.PutUvarint(buf[index:], h.DataPos)
+ index += binary.PutVarint(buf[index:], h.FileID)
+
+ copy(buf[index:], h.Key)
+ index += keySize
+
+ return buf[:index]
+}
+
+// Decode decodes the hint entry from bytes
+func (h *HintEntry) Decode(buf []byte) error {
+ if len(buf) == 0 {
+ return ErrHintFileEntryInvalid
+ }
+
+ index := 0
+
+ bucketId, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.BucketId = bucketId
+
+ keySize, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.KeySize = uint32(keySize)
+
+ valueSize, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.ValueSize = uint32(valueSize)
+
+ timestamp, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.Timestamp = timestamp
+
+ ttl, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.TTL = uint32(ttl)
+
+ flag, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.Flag = uint16(flag)
+
+ status, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.Status = uint16(status)
+
+ ds, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.Ds = uint16(ds)
+
+ dataPos, n := binary.Uvarint(buf[index:])
+ if n <= 0 {
+ return ErrHintFileCorrupted
+ }
+ index += n
+ h.DataPos = dataPos
+
+ fileID, n, err := decodeCompatInt64(buf[index:])
+ if err != nil {
+ return err
+ }
+ index += n
+ h.FileID = fileID
+
+ if len(buf) < index+int(h.KeySize) {
+ return ErrHintFileCorrupted
+ }
+
+ h.Key = make([]byte, h.KeySize)
+ copy(h.Key, buf[index:index+int(h.KeySize)])
+
+ return nil
+}
+
+// HintFileReader is used to read hint files
+type HintFileReader struct {
+ file *os.File
+ reader *bufio.Reader
+}
+
+// Open opens a hint file for reading
+func (r *HintFileReader) Open(path string) error {
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+
+ r.file = file
+ r.reader = bufio.NewReader(file)
+
+ return nil
+}
+
+// Read reads a hint entry from the file
+func (r *HintFileReader) Read() (*HintEntry, error) {
+ if r.reader == nil {
+ return nil, ErrHintFileEntryInvalid
+ }
+
+ entry := &HintEntry{}
+ fieldsRead := 0
+
+ readUvarint := func() (uint64, error) {
+ val, err := binary.ReadUvarint(r.reader)
+ if err != nil {
+ if err == io.EOF {
+ if fieldsRead == 0 {
+ return 0, io.EOF
+ }
+ return 0, ErrHintFileCorrupted
+ }
+ if err == io.ErrUnexpectedEOF {
+ return 0, ErrHintFileCorrupted
+ }
+ return 0, err
+ }
+ fieldsRead++
+ return val, nil
+ }
+
+ var err error
+ if entry.BucketId, err = readUvarint(); err != nil {
+ return nil, err
+ }
+
+ keySize, err := readUvarint()
+ if err != nil {
+ return nil, err
+ }
+ entry.KeySize = uint32(keySize)
+
+ valueSize, err := readUvarint()
+ if err != nil {
+ return nil, err
+ }
+ entry.ValueSize = uint32(valueSize)
+
+ if entry.Timestamp, err = readUvarint(); err != nil {
+ return nil, err
+ }
+
+ ttl, err := readUvarint()
+ if err != nil {
+ return nil, err
+ }
+ entry.TTL = uint32(ttl)
+
+ flag, err := readUvarint()
+ if err != nil {
+ return nil, err
+ }
+ entry.Flag = uint16(flag)
+
+ status, err := readUvarint()
+ if err != nil {
+ return nil, err
+ }
+ entry.Status = uint16(status)
+
+ ds, err := readUvarint()
+ if err != nil {
+ return nil, err
+ }
+ entry.Ds = uint16(ds)
+
+ if entry.DataPos, err = readUvarint(); err != nil {
+ return nil, err
+ }
+
+ fileID, err := readCompatInt64(r.reader, &fieldsRead)
+ if err != nil {
+ return nil, err
+ }
+ entry.FileID = fileID
+
+ if entry.KeySize > 0 {
+ entry.Key = make([]byte, entry.KeySize)
+ if _, err := io.ReadFull(r.reader, entry.Key); err != nil {
+ if err == io.EOF || err == io.ErrUnexpectedEOF {
+ return nil, ErrHintFileCorrupted
+ }
+ return nil, err
+ }
+ }
+
+ return entry, nil
+}
+
+func decodeCompatInt64(buf []byte) (int64, int, error) {
+ if len(buf) == 0 {
+ return 0, 0, ErrHintFileCorrupted
+ }
+ fileID, n := binary.Varint(buf)
+ if n > 0 {
+ var encoded [binary.MaxVarintLen64]byte
+ m := binary.PutVarint(encoded[:], fileID)
+ if n == m && bytes.Equal(buf[:n], encoded[:m]) {
+ return fileID, n, nil
+ }
+ }
+
+ uval, n2 := binary.Uvarint(buf)
+ if n2 > 0 {
+ return int64(uval), n2, nil
+ }
+
+ return 0, 0, ErrHintFileCorrupted
+}
+
+func readCompatInt64(r *bufio.Reader, fieldsRead *int) (int64, error) {
+ var scratch [binary.MaxVarintLen64]byte
+ var i int
+
+ for {
+ b, err := r.ReadByte()
+ if err != nil {
+ if err == io.EOF {
+ if i == 0 {
+ if fieldsRead != nil && *fieldsRead == 0 {
+ return 0, io.EOF
+ }
+ return 0, ErrHintFileCorrupted
+ }
+ return 0, ErrHintFileCorrupted
+ }
+ if err == io.ErrUnexpectedEOF {
+ return 0, ErrHintFileCorrupted
+ }
+ return 0, err
+ }
+
+ scratch[i] = b
+ i++
+
+ if b < 0x80 || i == len(scratch) {
+ break
+ }
+ }
+
+ if i == len(scratch) && scratch[i-1]&0x80 != 0 {
+ return 0, ErrHintFileCorrupted
+ }
+
+ val, n, err := decodeCompatInt64(scratch[:i])
+ if err != nil {
+ return 0, err
+ }
+ if n != i {
+ return 0, ErrHintFileCorrupted
+ }
+
+ if fieldsRead != nil {
+ (*fieldsRead)++
+ }
+
+ return val, nil
+}
+
+// Close closes the hint file
+func (r *HintFileReader) Close() error {
+ if r.file != nil {
+ return r.file.Close()
+ }
+ return nil
+}
+
+// HintFileWriter is used to write hint files
+type HintFileWriter struct {
+ file *os.File
+ writer *bufio.Writer
+}
+
+// Create creates a hint file for writing
+func (w *HintFileWriter) Create(path string) error {
+ file, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+
+ w.file = file
+ w.writer = bufio.NewWriter(file)
+
+ return nil
+}
+
+// Write writes a hint entry to the file
+func (w *HintFileWriter) Write(entry *HintEntry) error {
+ if entry == nil {
+ return ErrHintFileEntryInvalid
+ }
+
+ data := entry.Encode()
+ _, err := w.writer.Write(data)
+ return err
+}
+
+// Sync flushes the buffer and syncs the file to disk
+func (w *HintFileWriter) Sync() error {
+ if w.writer != nil {
+ if err := w.writer.Flush(); err != nil {
+ return err
+ }
+ }
+
+ if w.file != nil {
+ return w.file.Sync()
+ }
+
+ return nil
+}
+
+// Close closes the hint file
+func (w *HintFileWriter) Close() error {
+ var err error
+
+ if w.writer != nil {
+ if flushErr := w.writer.Flush(); flushErr != nil {
+ err = flushErr
+ }
+ }
+
+ if w.file != nil {
+ if closeErr := w.file.Close(); closeErr != nil && err == nil {
+ err = closeErr
+ }
+ }
+
+ return err
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/index.go b/vendor/github.com/nutsdb/nutsdb/index.go
index 743fb86a33..15571390ca 100644
--- a/vendor/github.com/nutsdb/nutsdb/index.go
+++ b/vendor/github.com/nutsdb/nutsdb/index.go
@@ -14,8 +14,10 @@
package nutsdb
+import "github.com/nutsdb/nutsdb/internal/data"
+
type IdxType interface {
- BTree | Set | SortedSet | List
+ data.BTree | data.Set | SortedSet | data.List
}
type defaultOp[T IdxType] struct {
@@ -51,32 +53,33 @@ func (op *defaultOp[T]) rangeIdx(f func(elem *T)) {
}
type ListIdx struct {
- *defaultOp[List]
+ *defaultOp[data.List]
+ opts Options
}
-func (idx ListIdx) getWithDefault(id BucketId) *List {
- return idx.defaultOp.computeIfAbsent(id, func() *List {
- return NewList()
+func (idx ListIdx) getWithDefault(id BucketId) *data.List {
+ return idx.defaultOp.computeIfAbsent(id, func() *data.List {
+ return data.NewList(idx.opts.ListImpl.toInternal())
})
}
type BTreeIdx struct {
- *defaultOp[BTree]
+ *defaultOp[data.BTree]
}
-func (idx BTreeIdx) getWithDefault(id BucketId) *BTree {
- return idx.defaultOp.computeIfAbsent(id, func() *BTree {
- return NewBTree()
+func (idx BTreeIdx) getWithDefault(id BucketId) *data.BTree {
+ return idx.defaultOp.computeIfAbsent(id, func() *data.BTree {
+ return data.NewBTree()
})
}
type SetIdx struct {
- *defaultOp[Set]
+ *defaultOp[data.Set]
}
-func (idx SetIdx) getWithDefault(id BucketId) *Set {
- return idx.defaultOp.computeIfAbsent(id, func() *Set {
- return NewSet()
+func (idx SetIdx) getWithDefault(id BucketId) *data.Set {
+ return idx.defaultOp.computeIfAbsent(id, func() *data.Set {
+ return data.NewSet()
})
}
@@ -95,13 +98,19 @@ type index struct {
bTree BTreeIdx
set SetIdx
sortedSet SortedSetIdx
+ opts Options // Store options for creating new data structures
}
func newIndex() *index {
+ return newIndexWithOptions(DefaultOptions)
+}
+
+func newIndexWithOptions(opts Options) *index {
i := new(index)
- i.list = ListIdx{&defaultOp[List]{idx: map[BucketId]*List{}}}
- i.bTree = BTreeIdx{&defaultOp[BTree]{idx: map[BucketId]*BTree{}}}
- i.set = SetIdx{&defaultOp[Set]{idx: map[BucketId]*Set{}}}
+ i.opts = opts
+ i.list = ListIdx{defaultOp: &defaultOp[data.List]{idx: map[BucketId]*data.List{}}, opts: opts}
+ i.bTree = BTreeIdx{&defaultOp[data.BTree]{idx: map[BucketId]*data.BTree{}}}
+ i.set = SetIdx{&defaultOp[data.Set]{idx: map[BucketId]*data.Set{}}}
i.sortedSet = SortedSetIdx{&defaultOp[SortedSet]{idx: map[BucketId]*SortedSet{}}}
return i
}
diff --git a/vendor/github.com/nutsdb/nutsdb/btree.go b/vendor/github.com/nutsdb/nutsdb/internal/data/btree.go
similarity index 60%
rename from vendor/github.com/nutsdb/nutsdb/btree.go
rename to vendor/github.com/nutsdb/nutsdb/internal/data/btree.go
index c7468ecfc4..34f3c9a6c7 100644
--- a/vendor/github.com/nutsdb/nutsdb/btree.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/data/btree.go
@@ -12,56 +12,42 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package nutsdb
+package data
import (
"bytes"
- "errors"
"regexp"
"github.com/tidwall/btree"
)
-// ErrKeyNotFound is returned when the key is not in the b tree.
-var ErrKeyNotFound = errors.New("key not found")
-
-type Item struct {
- key []byte
- record *Record
-}
-
type BTree struct {
- btree *btree.BTreeG[*Item]
+ btree *btree.BTreeG[*Item[Record]]
}
func NewBTree() *BTree {
return &BTree{
- btree: btree.NewBTreeG[*Item](func(a, b *Item) bool {
- return bytes.Compare(a.key, b.key) == -1
+ btree: btree.NewBTreeG(func(a, b *Item[Record]) bool {
+ return bytes.Compare(a.Key, b.Key) == -1
}),
}
}
func (bt *BTree) Find(key []byte) (*Record, bool) {
- item, ok := bt.btree.Get(&Item{key: key})
+ item, ok := bt.btree.Get(NewItem[Record](key, nil))
if ok {
- return item.record, ok
+ return item.Record, ok
}
return nil, ok
}
-func (bt *BTree) Insert(record *Record) bool {
- _, replaced := bt.btree.Set(&Item{key: record.Key, record: record})
- return replaced
-}
-
func (bt *BTree) InsertRecord(key []byte, record *Record) bool {
- _, replaced := bt.btree.Set(&Item{key: key, record: record})
+ _, replaced := bt.btree.Set(NewItem(key, record))
return replaced
}
func (bt *BTree) Delete(key []byte) bool {
- _, deleted := bt.btree.Delete(&Item{key: key})
+ _, deleted := bt.btree.Delete(NewItem[Record](key, nil))
return deleted
}
@@ -70,13 +56,13 @@ func (bt *BTree) All() []*Record {
records := make([]*Record, len(items))
for i, item := range items {
- records[i] = item.record
+ records[i] = item.Record
}
return records
}
-func (bt *BTree) AllItems() []*Item {
+func (bt *BTree) AllItems() []*Item[Record] {
items := bt.btree.Items()
return items
}
@@ -84,11 +70,11 @@ func (bt *BTree) AllItems() []*Item {
func (bt *BTree) Range(start, end []byte) []*Record {
records := make([]*Record, 0)
- bt.btree.Ascend(&Item{key: start}, func(item *Item) bool {
- if bytes.Compare(item.key, end) > 0 {
+ bt.btree.Ascend(&Item[Record]{Key: start}, func(item *Item[Record]) bool {
+ if bytes.Compare(item.Key, end) > 0 {
return false
}
- records = append(records, item.record)
+ records = append(records, item.Record)
return true
})
@@ -98,8 +84,8 @@ func (bt *BTree) Range(start, end []byte) []*Record {
func (bt *BTree) PrefixScan(prefix []byte, offset, limitNum int) []*Record {
records := make([]*Record, 0)
- bt.btree.Ascend(&Item{key: prefix}, func(item *Item) bool {
- if !bytes.HasPrefix(item.key, prefix) {
+ bt.btree.Ascend(&Item[Record]{Key: prefix}, func(item *Item[Record]) bool {
+ if !bytes.HasPrefix(item.Key, prefix) {
return false
}
@@ -108,7 +94,7 @@ func (bt *BTree) PrefixScan(prefix []byte, offset, limitNum int) []*Record {
return true
}
- records = append(records, item.record)
+ records = append(records, item.Record)
limitNum--
return limitNum != 0
@@ -122,8 +108,8 @@ func (bt *BTree) PrefixSearchScan(prefix []byte, reg string, offset, limitNum in
rgx := regexp.MustCompile(reg)
- bt.btree.Ascend(&Item{key: prefix}, func(item *Item) bool {
- if !bytes.HasPrefix(item.key, prefix) {
+ bt.btree.Ascend(&Item[Record]{Key: prefix}, func(item *Item[Record]) bool {
+ if !bytes.HasPrefix(item.Key, prefix) {
return false
}
@@ -132,11 +118,11 @@ func (bt *BTree) PrefixSearchScan(prefix []byte, reg string, offset, limitNum in
return true
}
- if !rgx.Match(bytes.TrimPrefix(item.key, prefix)) {
+ if !rgx.Match(bytes.TrimPrefix(item.Key, prefix)) {
return true
}
- records = append(records, item.record)
+ records = append(records, item.Record)
limitNum--
return limitNum != 0
@@ -149,18 +135,22 @@ func (bt *BTree) Count() int {
return bt.btree.Len()
}
-func (bt *BTree) PopMin() (*Item, bool) {
+func (bt *BTree) PopMin() (*Item[Record], bool) {
return bt.btree.PopMin()
}
-func (bt *BTree) PopMax() (*Item, bool) {
+func (bt *BTree) PopMax() (*Item[Record], bool) {
return bt.btree.PopMax()
}
-func (bt *BTree) Min() (*Item, bool) {
+func (bt *BTree) Min() (*Item[Record], bool) {
return bt.btree.Min()
}
-func (bt *BTree) Max() (*Item, bool) {
+func (bt *BTree) Max() (*Item[Record], bool) {
return bt.btree.Max()
}
+
+func (bt *BTree) Iter() btree.IterG[*Item[Record]] {
+ return bt.btree.Iter()
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/internal/data/doubly_linked_list.go b/vendor/github.com/nutsdb/nutsdb/internal/data/doubly_linked_list.go
new file mode 100644
index 0000000000..ad64fb04e2
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/internal/data/doubly_linked_list.go
@@ -0,0 +1,298 @@
+// Copyright 2023 The nutsdb Author. All rights reserved.
+//
+// Licensed 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 data
+
+import (
+ "bytes"
+ "container/list"
+ "math"
+ "regexp"
+)
+
+const (
+ InitialListSeq = math.MaxUint64 / 2
+)
+
+// DoublyLinkedList represents a doubly linked list optimized for head/tail operations.
+// It uses Go's standard container/list without additional index structures.
+// Best suited for workloads dominated by LPush/RPush/LPop/RPop operations.
+// Note: Find and Delete operations require O(n) traversal.
+type DoublyLinkedList struct {
+ list *list.List // standard library doubly linked list
+}
+
+// NewDoublyLinkedList creates a new doubly linked list
+func NewDoublyLinkedList() *DoublyLinkedList {
+ return &DoublyLinkedList{
+ list: list.New(),
+ }
+}
+
+// InsertRecord inserts a record with the given key in sorted order by key (sequence number).
+// Optimized for head/tail insertions (LPush/RPush pattern).
+// Warning: Middle insertions require O(n) traversal. Check for duplicates requires O(n) scan.
+func (dll *DoublyLinkedList) InsertRecord(key []byte, record *Record) bool {
+ newElem := &Item[Record]{
+ Key: key,
+ Record: record,
+ }
+
+ // Empty list - just insert
+ if dll.list.Len() == 0 {
+ dll.list.PushBack(newElem)
+ return false
+ }
+
+ // Check if inserting at head or tail (common case for List operations)
+ front := dll.list.Front().Value.(*Item[Record])
+ back := dll.list.Back().Value.(*Item[Record])
+
+ // Check for duplicate at head
+ if bytes.Equal(key, front.Key) {
+ front.Record = record
+ return true
+ }
+
+ // Insert at head if key is smaller than front
+ if bytes.Compare(key, front.Key) < 0 {
+ dll.list.PushFront(newElem)
+ return false
+ }
+
+ // Check for duplicate at tail
+ if bytes.Equal(key, back.Key) {
+ back.Record = record
+ return true
+ }
+
+ // Insert at tail if key is larger than back
+ if bytes.Compare(key, back.Key) > 0 {
+ dll.list.PushBack(newElem)
+ return false
+ }
+
+ // Middle insertion: find the correct position (O(n) operation)
+ // This should be rare in typical List usage patterns
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ cmp := bytes.Compare(key, elem.Key)
+
+ if cmp == 0 {
+ // Update existing element
+ elem.Record = record
+ return true
+ }
+
+ if cmp < 0 {
+ // Insert before current element
+ dll.list.InsertBefore(newElem, e)
+ return false
+ }
+ }
+
+ // Fallback (shouldn't reach here given the checks above)
+ dll.list.PushBack(newElem)
+ return false
+}
+
+// Delete removes a node with the given key.
+// Warning: This is an O(n) operation since we don't maintain an index.
+// For List use cases, prefer PopMin/PopMax for head/tail deletions.
+func (dll *DoublyLinkedList) Delete(key []byte) bool {
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ if bytes.Equal(elem.Key, key) {
+ dll.list.Remove(e)
+ return true
+ }
+ }
+ return false
+}
+
+// Find returns the record with the given key.
+// Warning: This is an O(n) operation since we don't maintain an index.
+func (dll *DoublyLinkedList) Find(key []byte) (*Record, bool) {
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ if bytes.Equal(elem.Key, key) {
+ return elem.Record, true
+ }
+ }
+ return nil, false
+}
+
+// Min returns the first element (smallest key)
+func (dll *DoublyLinkedList) Min() (*Item[Record], bool) {
+ front := dll.list.Front()
+ if front == nil {
+ return nil, false
+ }
+ elem := front.Value.(*Item[Record])
+ return &Item[Record]{
+ Key: elem.Key,
+ Record: elem.Record,
+ }, true
+}
+
+// Max returns the last element (largest key)
+func (dll *DoublyLinkedList) Max() (*Item[Record], bool) {
+ back := dll.list.Back()
+ if back == nil {
+ return nil, false
+ }
+ elem := back.Value.(*Item[Record])
+ return &Item[Record]{
+ Key: elem.Key,
+ Record: elem.Record,
+ }, true
+}
+
+// PopMin removes and returns the first element
+func (dll *DoublyLinkedList) PopMin() (*Item[Record], bool) {
+ front := dll.list.Front()
+ if front == nil {
+ return nil, false
+ }
+
+ elem := front.Value.(*Item[Record])
+ dll.list.Remove(front)
+
+ // Construct result - keep fields in same order as PopMax for consistency
+ return &Item[Record]{
+ Key: elem.Key,
+ Record: elem.Record,
+ }, true
+}
+
+// PopMax removes and returns the last element
+func (dll *DoublyLinkedList) PopMax() (*Item[Record], bool) {
+ back := dll.list.Back()
+ if back == nil {
+ return nil, false
+ }
+
+ elem := back.Value.(*Item[Record])
+ dll.list.Remove(back)
+
+ return &Item[Record]{
+ Key: elem.Key,
+ Record: elem.Record,
+ }, true
+}
+
+// All returns all records in order
+func (dll *DoublyLinkedList) All() []*Record {
+ records := make([]*Record, 0, dll.list.Len())
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ records = append(records, elem.Record)
+ }
+ return records
+}
+
+// AllItems returns all items in order
+func (dll *DoublyLinkedList) AllItems() []*Item[Record] {
+ items := make([]*Item[Record], 0, dll.list.Len())
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ items = append(items, &Item[Record]{
+ Key: elem.Key,
+ Record: elem.Record,
+ })
+ }
+ return items
+}
+
+// Count returns the number of elements
+func (dll *DoublyLinkedList) Count() int {
+ return dll.list.Len()
+}
+
+// Range returns records within the given key range [start, end]
+func (dll *DoublyLinkedList) Range(start, end []byte) []*Record {
+ records := make([]*Record, 0)
+
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ if bytes.Compare(elem.Key, start) >= 0 && bytes.Compare(elem.Key, end) <= 0 {
+ records = append(records, elem.Record)
+ }
+ if bytes.Compare(elem.Key, end) > 0 {
+ break
+ }
+ }
+
+ return records
+}
+
+// PrefixScan scans records with the given prefix
+func (dll *DoublyLinkedList) PrefixScan(prefix []byte, offset, limitNum int) []*Record {
+ records := make([]*Record, 0)
+
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ if bytes.HasPrefix(elem.Key, prefix) {
+ if offset > 0 {
+ offset--
+ } else {
+ records = append(records, elem.Record)
+ limitNum--
+ if limitNum == 0 {
+ break
+ }
+ }
+ }
+ }
+
+ return records
+}
+
+// PrefixSearchScan scans records with the given prefix and regex pattern
+func (dll *DoublyLinkedList) PrefixSearchScan(prefix []byte, reg string, offset, limitNum int) []*Record {
+ records := make([]*Record, 0)
+ rgx, err := regexp.Compile(reg)
+ if err != nil {
+ return records
+ }
+
+ for e := dll.list.Front(); e != nil; e = e.Next() {
+ elem := e.Value.(*Item[Record])
+ if !bytes.HasPrefix(elem.Key, prefix) {
+ continue
+ }
+
+ if offset > 0 {
+ offset--
+ continue
+ }
+
+ if !rgx.Match(bytes.TrimPrefix(elem.Key, prefix)) {
+ continue
+ }
+
+ records = append(records, elem.Record)
+ limitNum--
+ if limitNum == 0 {
+ break
+ }
+ }
+
+ return records
+}
+
+// Insert is an alias for InsertRecord (for compatibility with BTree interface)
+func (dll *DoublyLinkedList) Insert(record *Record) bool {
+ return dll.InsertRecord(record.Key, record)
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/internal/data/item.go b/vendor/github.com/nutsdb/nutsdb/internal/data/item.go
new file mode 100644
index 0000000000..caf82b9e24
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/internal/data/item.go
@@ -0,0 +1,13 @@
+package data
+
+type Item[T any] struct {
+ Key []byte
+ Record *T
+}
+
+func NewItem[T any](key []byte, record *T) *Item[T] {
+ return &Item[T]{
+ Key: key,
+ Record: record,
+ }
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/internal/data/list.go b/vendor/github.com/nutsdb/nutsdb/internal/data/list.go
new file mode 100644
index 0000000000..2ed513aa47
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/internal/data/list.go
@@ -0,0 +1,551 @@
+package data
+
+import (
+ "errors"
+ "time"
+
+ "github.com/nutsdb/nutsdb/internal/utils"
+)
+
+var (
+ // ErrListNotFound is returned when the list not found.
+ ErrListNotFound = errors.New("the list not found")
+
+ // ErrCount is returned when count is error.
+ ErrCount = errors.New("err count")
+
+ // ErrEmptyList is returned when the list is empty.
+ ErrEmptyList = errors.New("the list is empty")
+
+ // ErrStartOrEnd is returned when start > end
+ ErrStartOrEnd = errors.New("start or end error")
+)
+
+type ListImplementationType int
+
+const (
+ // ListImplDoublyLinkedList uses doubly linked list implementation (default).
+ // Advantages: O(1) head/tail operations, lower memory overhead
+ // Best for: High-frequency LPush/RPush/LPop/RPop operations
+ ListImplDoublyLinkedList = iota
+
+ // ListImplBTree uses BTree implementation.
+ // Advantages: O(log n + k) range queries, efficient random access
+ // Best for: Frequent range queries or indexed access patterns
+ ListImplBTree
+)
+
+// HeadTailSeq list head and tail seq num
+type HeadTailSeq struct {
+ Head uint64
+ Tail uint64
+}
+
+func (seq *HeadTailSeq) GenerateSeq(isLeft bool) uint64 {
+ var res uint64
+ if isLeft {
+ res = seq.Head
+ seq.Head--
+ } else {
+ res = seq.Tail
+ seq.Tail++
+ }
+
+ return res
+}
+
+// ListStructure defines the interface for List storage implementations.
+// It supports multiple implementations: BTree, DoublyLinkedList, SkipList, etc.
+// This interface enables users to choose the most suitable implementation based on their use case:
+// - DoublyLinkedList: O(1) head/tail operations, optimal for LPush/RPush/LPop/RPop
+// - BTree: O(log n) operations, better for range queries and random access
+type ListStructure interface {
+ // InsertRecord inserts a record with the given key (sequence number in big-endian format).
+ // Returns true if an existing record was replaced, false if a new record was inserted.
+ InsertRecord(key []byte, record *Record) bool
+
+ // Delete removes the record with the given key.
+ // Returns true if the record was found and deleted, false otherwise.
+ Delete(key []byte) bool
+
+ // Find retrieves the record with the given key.
+ // Returns the record and true if found, nil and false otherwise.
+ Find(key []byte) (*Record, bool)
+
+ // Min returns the item with the smallest key (head of the list).
+ // Returns the item and true if the list is not empty, nil and false otherwise.
+ Min() (*Item[Record], bool)
+
+ // Max returns the item with the largest key (tail of the list).
+ // Returns the item and true if the list is not empty, nil and false otherwise.
+ Max() (*Item[Record], bool)
+
+ // All returns all records in ascending key order.
+ All() []*Record
+
+ // AllItems returns all items (key + record pairs) in ascending key order.
+ AllItems() []*Item[Record]
+
+ // Count returns the number of elements in the list.
+ Count() int
+
+ // Range returns records with keys in the range [start, end] (inclusive).
+ Range(start, end []byte) []*Record
+
+ // PrefixScan scans records with keys matching the given prefix.
+ // offset: number of matching records to skip
+ // limitNum: maximum number of records to return
+ PrefixScan(prefix []byte, offset, limitNum int) []*Record
+
+ // PrefixSearchScan scans records with keys matching the given prefix and regex pattern.
+ // The regex is applied to the portion of the key after removing the prefix.
+ // offset: number of matching records to skip
+ // limitNum: maximum number of records to return
+ PrefixSearchScan(prefix []byte, reg string, offset, limitNum int) []*Record
+
+ // PopMin removes and returns the item with the smallest key.
+ // Returns the item and true if the list is not empty, nil and false otherwise.
+ PopMin() (*Item[Record], bool)
+
+ // PopMax removes and returns the item with the largest key.
+ // Returns the item and true if the list is not empty, nil and false otherwise.
+ PopMax() (*Item[Record], bool)
+}
+
+// Compile-time interface implementation checks
+var (
+ _ ListStructure = (*BTree)(nil)
+ _ ListStructure = (*DoublyLinkedList)(nil)
+)
+
+// BTree represents the btree.
+
+// List represents the list.
+type List struct {
+ Items map[string]ListStructure
+ TTL map[string]uint32
+ TimeStamp map[string]uint64
+ Seq map[string]*HeadTailSeq
+ ListImpl ListImplementationType
+}
+
+func NewList(listImpl ListImplementationType) *List {
+ return &List{
+ Items: make(map[string]ListStructure),
+ TTL: make(map[string]uint32),
+ TimeStamp: make(map[string]uint64),
+ Seq: make(map[string]*HeadTailSeq),
+ ListImpl: listImpl,
+ }
+}
+
+// CreateListStructure creates a new list storage structure based on configuration.
+func (l *List) CreateListStructure() ListStructure {
+ switch l.ListImpl {
+ case ListImplBTree:
+ return NewBTree()
+ case ListImplDoublyLinkedList:
+ return NewDoublyLinkedList()
+ default:
+ // Default to DoublyLinkedList for safety
+ return NewDoublyLinkedList()
+ }
+}
+
+func (l *List) LPush(key string, r *Record) error {
+ return l.Push(key, r, true)
+}
+
+func (l *List) RPush(key string, r *Record) error {
+ return l.Push(key, r, false)
+}
+
+func (l *List) Push(key string, r *Record, isLeft bool) error {
+ // key is seq + user_key
+ userKey, curSeq := utils.DecodeListKey([]byte(key))
+ userKeyStr := string(userKey)
+ if l.IsExpire(userKeyStr) {
+ return ErrListNotFound
+ }
+
+ list, ok := l.Items[userKeyStr]
+ if !ok {
+ l.Items[userKeyStr] = l.CreateListStructure()
+ list = l.Items[userKeyStr]
+ }
+
+ // Initialize seq if not exists
+ if _, ok := l.Seq[userKeyStr]; !ok {
+ l.Seq[userKeyStr] = &HeadTailSeq{Head: InitialListSeq, Tail: InitialListSeq + 1}
+ }
+
+ list.InsertRecord(utils.ConvertUint64ToBigEndianBytes(curSeq), r)
+
+ // Update seq boundaries to track the next insertion positions
+ // This is important for recovery scenarios where we rebuild the index
+ // Head and Tail should always represent the next available positions for insertion
+ seq := l.Seq[userKeyStr]
+ if isLeft {
+ // LPush: Head should be the next available position on the left
+ // If current seq is the actual head, set Head to current seq - 1
+ if curSeq <= seq.Head {
+ seq.Head = curSeq - 1
+ }
+ } else {
+ // RPush: Tail should be the next available position on the right
+ // If current seq is at or beyond current tail, update Tail accordingly
+ if curSeq >= seq.Tail {
+ seq.Tail = curSeq + 1
+ }
+ }
+
+ return nil
+}
+
+func (l *List) LPop(key string) (*Record, error) {
+ if l.IsExpire(key) {
+ return nil, ErrListNotFound
+ }
+
+ list, ok := l.Items[key]
+ if !ok {
+ return nil, ErrListNotFound
+ }
+
+ // Use PopMin for efficient O(1) head removal
+ item, ok := list.PopMin()
+ if !ok {
+ return nil, ErrEmptyList
+ }
+
+ // After LPop, Head should point to the next element's position
+ // Note: We don't update Head here because it represents "next push position"
+ // The popped element's sequence is already consumed
+ return item.Record, nil
+}
+
+// RPop removes and returns the last element of the list stored at key.
+func (l *List) RPop(key string) (*Record, error) {
+ if l.IsExpire(key) {
+ return nil, ErrListNotFound
+ }
+
+ list, ok := l.Items[key]
+ if !ok {
+ return nil, ErrListNotFound
+ }
+
+ // Use PopMax for efficient O(1) tail removal
+ item, ok := list.PopMax()
+ if !ok {
+ return nil, ErrEmptyList
+ }
+
+ // After RPop, Tail should point to the next element's position
+ // Note: We don't update Tail here because it represents "next push position"
+ // The popped element's sequence is already consumed
+ return item.Record, nil
+}
+
+func (l *List) LPeek(key string) (*Item[Record], error) {
+ return l.peek(key, true)
+}
+
+func (l *List) RPeek(key string) (*Item[Record], error) {
+ return l.peek(key, false)
+}
+
+func (l *List) peek(key string, isLeft bool) (*Item[Record], error) {
+ if l.IsExpire(key) {
+ return nil, ErrListNotFound
+ }
+ list, ok := l.Items[key]
+ if !ok {
+ return nil, ErrListNotFound
+ }
+
+ if isLeft {
+ item, ok := list.Min()
+ if ok {
+ return item, nil
+ }
+ } else {
+ item, ok := list.Max()
+ if ok {
+ return item, nil
+ }
+ }
+
+ return nil, ErrEmptyList
+}
+
+// LRange returns the specified elements of the list stored at key [start,end]
+func (l *List) LRange(key string, start, end int) ([]*Record, error) {
+ size, err := l.Size(key)
+ if err != nil || size == 0 {
+ return nil, err
+ }
+
+ start, end, err = checkBounds(start, end, size)
+ if err != nil {
+ return nil, err
+ }
+
+ var res []*Record
+ allRecords := l.Items[key].All()
+ for i, item := range allRecords {
+ if i >= start && i <= end {
+ res = append(res, item)
+ }
+ }
+
+ return res, nil
+}
+
+// GetRemoveIndexes returns a slice of indices to be removed from the list based on the count
+func (l *List) GetRemoveIndexes(key string, count int, cmp func(r *Record) (bool, error)) ([][]byte, error) {
+ if l.IsExpire(key) {
+ return nil, ErrListNotFound
+ }
+
+ list, ok := l.Items[key]
+
+ if !ok {
+ return nil, ErrListNotFound
+ }
+
+ var res [][]byte
+ var allItems []*Item[Record]
+ if count == 0 {
+ count = list.Count()
+ }
+
+ allItems = l.Items[key].AllItems()
+ if count > 0 {
+ for _, item := range allItems {
+ if count <= 0 {
+ break
+ }
+ r := item.Record
+ ok, err := cmp(r)
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ res = append(res, item.Key)
+ count--
+ }
+ }
+ } else {
+ for i := len(allItems) - 1; i >= 0; i-- {
+ if count >= 0 {
+ break
+ }
+ r := allItems[i].Record
+ ok, err := cmp(r)
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ res = append(res, allItems[i].Key)
+ count++
+ }
+ }
+ }
+
+ return res, nil
+}
+
+// LRem removes the first count occurrences of elements equal to value from the list stored at key.
+// The count argument influences the operation in the following ways:
+// count > 0: Remove elements equal to value moving from head to tail.
+// count < 0: Remove elements equal to value moving from tail to head.
+// count = 0: Remove all elements equal to value.
+func (l *List) LRem(key string, count int, cmp func(r *Record) (bool, error)) error {
+ removeIndexes, err := l.GetRemoveIndexes(key, count, cmp)
+ if err != nil {
+ return err
+ }
+
+ list := l.Items[key]
+ for _, idx := range removeIndexes {
+ list.Delete(idx)
+ }
+
+ return nil
+}
+
+// LTrim trim an existing list so that it will contain only the specified range of elements specified.
+func (l *List) LTrim(key string, start, end int) error {
+ if l.IsExpire(key) {
+ return ErrListNotFound
+ }
+ if _, ok := l.Items[key]; !ok {
+ return ErrListNotFound
+ }
+
+ list := l.Items[key]
+ allItems := list.AllItems()
+ for i, item := range allItems {
+ if i < start || i > end {
+ list.Delete(item.Key)
+ }
+ }
+
+ return nil
+}
+
+// LRemByIndex remove the list element at specified index
+func (l *List) LRemByIndex(key string, indexes []int) error {
+ if l.IsExpire(key) {
+ return ErrListNotFound
+ }
+
+ idxes := l.GetValidIndexes(key, indexes)
+ if len(idxes) == 0 {
+ return nil
+ }
+
+ list := l.Items[key]
+ allItems := list.AllItems()
+ for i, item := range allItems {
+ if _, ok := idxes[i]; ok {
+ list.Delete(item.Key)
+ }
+ }
+
+ return nil
+}
+
+func (l *List) GetValidIndexes(key string, indexes []int) map[int]struct{} {
+ idxes := make(map[int]struct{})
+ listLen, err := l.Size(key)
+ if err != nil || listLen == 0 {
+ return idxes
+ }
+
+ for _, idx := range indexes {
+ if idx < 0 || idx >= listLen {
+ continue
+ }
+ idxes[idx] = struct{}{}
+ }
+
+ return idxes
+}
+
+func (l *List) IsExpire(key string) bool {
+ if l == nil {
+ return false
+ }
+
+ _, ok := l.TTL[key]
+ if !ok {
+ return false
+ }
+
+ now := time.Now().Unix()
+ timestamp := l.TimeStamp[key]
+ if l.TTL[key] > 0 && uint64(l.TTL[key])+timestamp > uint64(now) || l.TTL[key] == uint32(0) {
+ return false
+ }
+
+ delete(l.Items, key)
+ delete(l.TTL, key)
+ delete(l.TimeStamp, key)
+ delete(l.Seq, key)
+
+ return true
+}
+
+func (l *List) Size(key string) (int, error) {
+ if l.IsExpire(key) {
+ return 0, ErrListNotFound
+ }
+ if _, ok := l.Items[key]; !ok {
+ return 0, ErrListNotFound
+ }
+
+ return l.Items[key].Count(), nil
+}
+
+func (l *List) IsEmpty(key string) (bool, error) {
+ size, err := l.Size(key)
+ if err != nil || size > 0 {
+ return false, err
+ }
+ return true, nil
+}
+
+func (l *List) GetListTTL(key string) (uint32, error) {
+ if l.IsExpire(key) {
+ return 0, ErrListNotFound
+ }
+
+ ttl := l.TTL[key]
+ timestamp := l.TimeStamp[key]
+ if ttl == 0 || timestamp == 0 {
+ return 0, nil
+ }
+
+ now := time.Now().Unix()
+ remain := timestamp + uint64(ttl) - uint64(now)
+
+ return uint32(remain), nil
+}
+
+func (l *List) ExpireList(key []byte, ttl uint32) {
+ l.TTL[string(key)] = ttl
+ l.TimeStamp[string(key)] = uint64(time.Now().Unix())
+}
+
+func (l *List) GeneratePushKey(key []byte, isLeft bool) []byte {
+ // 获取或创建HeadTailSeq
+ keyStr := string(key)
+ seq, ok := l.Seq[keyStr]
+ if !ok {
+ // 如果不存在,先尝试从现有项推断
+ if items, exists := l.Items[keyStr]; exists && items.Count() > 0 {
+ minSeq, okMinSeq := items.Min()
+ maxSeq, okMaxSeq := items.Max()
+ if !okMinSeq || !okMaxSeq {
+ seq = &HeadTailSeq{Head: InitialListSeq, Tail: InitialListSeq + 1}
+ } else {
+ seq = &HeadTailSeq{
+ Head: utils.ConvertBigEndianBytesToUint64(minSeq.Key) - 1,
+ Tail: utils.ConvertBigEndianBytesToUint64(maxSeq.Key) + 1,
+ }
+ }
+ } else {
+ seq = &HeadTailSeq{Head: InitialListSeq, Tail: InitialListSeq + 1}
+ }
+ l.Seq[keyStr] = seq
+ }
+
+ seqValue := seq.GenerateSeq(isLeft)
+ return utils.EncodeListKey(key, seqValue)
+}
+
+func checkBounds(start, end int, size int) (int, int, error) {
+ if start >= 0 && end < 0 {
+ end = size + end
+ }
+
+ if start < 0 && end > 0 {
+ start = size + start
+ }
+
+ if start < 0 && end < 0 {
+ start, end = size+start, size+end
+ }
+
+ if end >= size {
+ end = size - 1
+ }
+
+ if start > end {
+ return 0, 0, ErrStartOrEnd
+ }
+
+ return start, end, nil
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/record.go b/vendor/github.com/nutsdb/nutsdb/internal/data/record.go
similarity index 77%
rename from vendor/github.com/nutsdb/nutsdb/record.go
rename to vendor/github.com/nutsdb/nutsdb/internal/data/record.go
index 677298fd40..278feeb59a 100644
--- a/vendor/github.com/nutsdb/nutsdb/record.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/data/record.go
@@ -12,12 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package nutsdb
+package data
import (
+ "math/rand"
"time"
+
+ "github.com/nutsdb/nutsdb/internal/testutils"
)
+const Persistent uint32 = 0
+
// Record means item of indexes in memory
type Record struct {
Key []byte
@@ -95,3 +100,25 @@ func (r *Record) WithTxID(txID uint64) *Record {
r.TxID = txID
return r
}
+
+func GenerateRecords(count int) []*Record {
+ rand.Seed(time.Now().UnixNano())
+ records := make([]*Record, count)
+ for i := 0; i < count; i++ {
+ key := testutils.GetTestBytes(i)
+ val := testutils.GetRandomBytes(24)
+
+ record := &Record{
+ Key: key,
+ Value: val,
+ FileID: int64(i),
+ DataPos: uint64(rand.Uint32()),
+ ValueSize: uint32(len(val)),
+ Timestamp: uint64(time.Now().Unix()),
+ TTL: uint32(rand.Intn(3600)),
+ TxID: uint64(rand.Intn(1000)),
+ }
+ records[i] = record
+ }
+ return records
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/set.go b/vendor/github.com/nutsdb/nutsdb/internal/data/set.go
similarity index 95%
rename from vendor/github.com/nutsdb/nutsdb/set.go
rename to vendor/github.com/nutsdb/nutsdb/internal/data/set.go
index 943b7f4369..e4cbb5075c 100644
--- a/vendor/github.com/nutsdb/nutsdb/set.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/data/set.go
@@ -12,11 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package nutsdb
+package data
import (
"errors"
- "hash/fnv"
+
+ "github.com/nutsdb/nutsdb/internal/utils"
)
var (
@@ -30,8 +31,6 @@ var (
ErrMemberEmpty = errors.New("item empty")
)
-var fnvHash = fnv.New32a()
-
type Set struct {
M map[string]map[uint32]*Record
}
@@ -51,7 +50,7 @@ func (s *Set) SAdd(key string, values [][]byte, records []*Record) error {
}
for i, value := range values {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return err
}
@@ -73,7 +72,7 @@ func (s *Set) SRem(key string, values ...[]byte) error {
}
for _, value := range values {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return err
}
@@ -152,7 +151,7 @@ func (s *Set) SIsMember(key string, value []byte) (bool, error) {
return false, ErrSetNotExist
}
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return false, err
}
@@ -173,7 +172,7 @@ func (s *Set) SAreMembers(key string, values ...[]byte) (bool, error) {
for _, value := range values {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return false, err
}
@@ -209,7 +208,7 @@ func (s *Set) SMove(key1, key2 string, value []byte) (bool, error) {
set1, set2 := s.M[key1], s.M[key2]
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return false, err
}
diff --git a/vendor/github.com/nutsdb/nutsdb/internal/fileio/const.go b/vendor/github.com/nutsdb/nutsdb/internal/fileio/const.go
new file mode 100644
index 0000000000..4f7c32d86b
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/internal/fileio/const.go
@@ -0,0 +1,17 @@
+package fileio
+
+import "os"
+
+const (
+ B = 1
+
+ KB = 1024 * B
+
+ MB = 1024 * KB
+
+ GB = 1024 * MB
+)
+
+var (
+ openFile = os.OpenFile
+)
diff --git a/vendor/github.com/nutsdb/nutsdb/fd_manager.go b/vendor/github.com/nutsdb/nutsdb/internal/fileio/fd_manager.go
similarity index 77%
rename from vendor/github.com/nutsdb/nutsdb/fd_manager.go
rename to vendor/github.com/nutsdb/nutsdb/internal/fileio/fd_manager.go
index d1975c7a14..4dc14d318f 100644
--- a/vendor/github.com/nutsdb/nutsdb/fd_manager.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/fileio/fd_manager.go
@@ -1,4 +1,4 @@
-package nutsdb
+package fileio
import (
"math"
@@ -16,20 +16,21 @@ const (
TooManyFileOpenErrSuffix = "too many open files"
)
-// fdManager hold a fd cache in memory, it lru based cache.
-type fdManager struct {
+// FdManager hold a fd cache in memory, it lru based cache.
+type FdManager struct {
lock sync.Mutex
- cache map[string]*FdInfo
fdList *doubleLinkedList
size int
cleanThresholdNums int
maxFdNums int
+
+ Cache map[string]*FdInfo
}
-// newFdm will return a fdManager object
-func newFdm(maxFdNums int, cleanThreshold float64) (fdm *fdManager) {
- fdm = &fdManager{
- cache: map[string]*FdInfo{},
+// NewFdm will return a fdManager object
+func NewFdm(maxFdNums int, cleanThreshold float64) (fdm *FdManager) {
+ fdm = &FdManager{
+ Cache: map[string]*FdInfo{},
fdList: initDoubleLinkedList(),
size: 0,
maxFdNums: DefaultMaxFileNums,
@@ -54,13 +55,17 @@ type FdInfo struct {
prev *FdInfo
}
-// getFd go through this method to get fd.
-func (fdm *fdManager) getFd(path string) (fd *os.File, err error) {
+func (fdInfo *FdInfo) Using() uint {
+ return fdInfo.using
+}
+
+// GetFd go through this method to get fd.
+func (fdm *FdManager) GetFd(path string) (fd *os.File, err error) {
fdm.lock.Lock()
defer fdm.lock.Unlock()
cleanPath := filepath.Clean(path)
- if fdInfo := fdm.cache[cleanPath]; fdInfo == nil {
- fd, err = os.OpenFile(cleanPath, os.O_CREATE|os.O_RDWR, 0o644)
+ if fdInfo := fdm.Cache[cleanPath]; fdInfo == nil {
+ fd, err = openFile(cleanPath, os.O_CREATE|os.O_RDWR, 0o644)
if err == nil {
// if the numbers of fd in cache larger than the cleanThreshold in config, we will clean useless fd in cache
if fdm.size >= fdm.cleanThresholdNums {
@@ -71,7 +76,7 @@ func (fdm *fdManager) getFd(path string) (fd *os.File, err error) {
return fd, nil
}
// add this fd to cache
- fdm.addToCache(fd, cleanPath)
+ fdm.AddToCache(fd, cleanPath)
return fd, nil
} else {
// determine if there are too many open files, we will first clean useless fd in cache and try open this file again
@@ -82,12 +87,12 @@ func (fdm *fdManager) getFd(path string) (fd *os.File, err error) {
return nil, err
}
// try open this file again,if it still returns err, we will show this error to user
- fd, err = os.OpenFile(cleanPath, os.O_CREATE|os.O_RDWR, 0o644)
+ fd, err = openFile(cleanPath, os.O_CREATE|os.O_RDWR, 0o644)
if err != nil {
return nil, err
}
// add to cache if open this file successfully
- fdm.addToCache(fd, cleanPath)
+ fdm.AddToCache(fd, cleanPath)
}
return fd, err
}
@@ -99,7 +104,7 @@ func (fdm *fdManager) getFd(path string) (fd *os.File, err error) {
}
// addToCache add fd to cache
-func (fdm *fdManager) addToCache(fd *os.File, cleanPath string) {
+func (fdm *FdManager) AddToCache(fd *os.File, cleanPath string) {
fdInfo := &FdInfo{
fd: fd,
using: 1,
@@ -107,15 +112,15 @@ func (fdm *fdManager) addToCache(fd *os.File, cleanPath string) {
}
fdm.fdList.addNode(fdInfo)
fdm.size++
- fdm.cache[cleanPath] = fdInfo
+ fdm.Cache[cleanPath] = fdInfo
}
// reduceUsing when RWManager object close, it will go through this method let fdm know it return the fd to cache
-func (fdm *fdManager) reduceUsing(path string) {
+func (fdm *FdManager) ReduceUsing(path string) {
fdm.lock.Lock()
defer fdm.lock.Unlock()
cleanPath := filepath.Clean(path)
- node, isExist := fdm.cache[cleanPath]
+ node, isExist := fdm.Cache[cleanPath]
if !isExist {
panic("unexpected the node is not in cache")
}
@@ -123,7 +128,7 @@ func (fdm *fdManager) reduceUsing(path string) {
}
// close means the cache.
-func (fdm *fdManager) close() error {
+func (fdm *FdManager) Close() error {
fdm.lock.Lock()
defer fdm.lock.Unlock()
node := fdm.fdList.tail.prev
@@ -132,7 +137,7 @@ func (fdm *fdManager) close() error {
if err != nil {
return err
}
- delete(fdm.cache, node.path)
+ delete(fdm.Cache, node.path)
fdm.size--
node = node.prev
}
@@ -178,7 +183,7 @@ func (list *doubleLinkedList) moveNodeToFront(node *FdInfo) {
list.addNode(node)
}
-func (fdm *fdManager) cleanUselessFd() error {
+func (fdm *FdManager) cleanUselessFd() error {
cleanNums := fdm.cleanThresholdNums
node := fdm.fdList.tail.prev
for node != nil && node != fdm.fdList.head && cleanNums > 0 {
@@ -190,7 +195,7 @@ func (fdm *fdManager) cleanUselessFd() error {
return err
}
fdm.size--
- delete(fdm.cache, node.path)
+ delete(fdm.Cache, node.path)
cleanNums--
}
node = nextItem
@@ -198,14 +203,14 @@ func (fdm *fdManager) cleanUselessFd() error {
return nil
}
-func (fdm *fdManager) closeByPath(path string) error {
+func (fdm *FdManager) CloseByPath(path string) error {
fdm.lock.Lock()
defer fdm.lock.Unlock()
- fdInfo, ok := fdm.cache[path]
+ fdInfo, ok := fdm.Cache[path]
if !ok {
return nil
}
- delete(fdm.cache, path)
+ delete(fdm.Cache, path)
fdm.fdList.removeNode(fdInfo)
return fdInfo.fd.Close()
diff --git a/vendor/github.com/nutsdb/nutsdb/internal/fileio/fileutils.go b/vendor/github.com/nutsdb/nutsdb/internal/fileio/fileutils.go
new file mode 100644
index 0000000000..81f50698b2
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/internal/fileio/fileutils.go
@@ -0,0 +1,21 @@
+package fileio
+
+import "os"
+
+// Truncate changes the size of the file.
+// If readOnly is true, it skips the file stat check and truncate operation,
+// which significantly improves read performance by avoiding unnecessary syscalls.
+func Truncate(path string, capacity int64, f *os.File, readOnly bool) error {
+ // Skip truncation for read-only operations to avoid expensive os.Stat syscall
+ if readOnly {
+ return nil
+ }
+
+ fileInfo, _ := os.Stat(path)
+ if fileInfo.Size() < capacity {
+ if err := f.Truncate(capacity); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/rwmanager.go b/vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanager.go
similarity index 78%
rename from vendor/github.com/nutsdb/nutsdb/rwmanager.go
rename to vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanager.go
index 75edc9914b..f931eb707f 100644
--- a/vendor/github.com/nutsdb/nutsdb/rwmanager.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanager.go
@@ -12,18 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package nutsdb
-
-// RWMode represents the read and write mode.
-type RWMode int
-
-const (
- // FileIO represents the read and write mode using standard I/O.
- FileIO RWMode = iota
-
- // MMap represents the read and write mode using mmap.
- MMap
-)
+package fileio
// RWManager represents an interface to a RWManager.
type RWManager interface {
diff --git a/vendor/github.com/nutsdb/nutsdb/rwmanger_fileio.go b/vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanger_fileio.go
similarity index 86%
rename from vendor/github.com/nutsdb/nutsdb/rwmanger_fileio.go
rename to vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanger_fileio.go
index 9bd7e186a6..869ec15d69 100644
--- a/vendor/github.com/nutsdb/nutsdb/rwmanger_fileio.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanger_fileio.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package nutsdb
+package fileio
import (
"os"
@@ -20,22 +20,22 @@ import (
// FileIORWManager represents the RWManager which using standard I/O.
type FileIORWManager struct {
- fd *os.File
- path string
- fdm *fdManager
- segmentSize int64
+ Fd *os.File
+ Path string
+ Fdm *FdManager
+ SegmentSize int64
}
// WriteAt writes len(b) bytes to the File starting at byte offset off.
// `WriteAt` is a wrapper of the *File.WriteAt.
func (fm *FileIORWManager) WriteAt(b []byte, off int64) (n int, err error) {
- return fm.fd.WriteAt(b, off)
+ return fm.Fd.WriteAt(b, off)
}
// ReadAt reads len(b) bytes from the File starting at byte offset off.
// `ReadAt` is a wrapper of the *File.ReadAt.
func (fm *FileIORWManager) ReadAt(b []byte, off int64) (n int, err error) {
- return fm.fd.ReadAt(b, off)
+ return fm.Fd.ReadAt(b, off)
}
// Sync commits the current contents of the file to stable storage.
@@ -43,20 +43,20 @@ func (fm *FileIORWManager) ReadAt(b []byte, off int64) (n int, err error) {
// of recently written data to disk.
// `Sync` is a wrapper of the *File.Sync.
func (fm *FileIORWManager) Sync() (err error) {
- return fm.fd.Sync()
+ return fm.Fd.Sync()
}
// Release is a wrapper around the reduceUsing method
func (fm *FileIORWManager) Release() (err error) {
- fm.fdm.reduceUsing(fm.path)
+ fm.Fdm.ReduceUsing(fm.Path)
return nil
}
func (fm *FileIORWManager) Size() int64 {
- return fm.segmentSize
+ return fm.SegmentSize
}
// Close will remove the cache in the fdm of the specified path, and call the close method of the os of the file
func (fm *FileIORWManager) Close() (err error) {
- return fm.fdm.closeByPath(fm.path)
+ return fm.Fdm.CloseByPath(fm.Path)
}
diff --git a/vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanger_mmap.go b/vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanger_mmap.go
new file mode 100644
index 0000000000..21274a85d7
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/internal/fileio/rwmanger_mmap.go
@@ -0,0 +1,182 @@
+// Copyright 2019 The nutsdb Author. All rights reserved.
+//
+// Licensed 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 fileio
+
+import (
+ "errors"
+ "os"
+ "runtime"
+ "sync"
+
+ "github.com/edsrzf/mmap-go"
+ "github.com/nutsdb/nutsdb/internal/utils"
+)
+
+var (
+ MmapBlockSize = int64(os.Getpagesize()) * 4
+
+ mmapRWManagerInstancesLock = sync.Mutex{}
+ mmapRWManagerInstances = make(map[string]*MMapRWManager)
+ mmapLRUCacheCapacity = 1024
+)
+
+var (
+ // ErrUnmappedMemory is returned when a function is called on unmapped memory
+ ErrUnmappedMemory = errors.New("unmapped memory")
+
+ // ErrIndexOutOfBound is returned when given offset out of mapped region
+ ErrIndexOutOfBound = errors.New("offset out of mapped region")
+)
+
+func GetMMapRWManager(fd *os.File, path string, fdm *FdManager, segmentSize int64) *MMapRWManager {
+ mmapRWManagerInstancesLock.Lock()
+ defer mmapRWManagerInstancesLock.Unlock()
+ mm, ok := mmapRWManagerInstances[path]
+ if ok {
+ return mm
+ }
+ mm = &MMapRWManager{
+ Fd: fd,
+ Path: path,
+ Fdm: fdm,
+ SegmentSize: segmentSize,
+ ReadCache: utils.NewLruCache(mmapLRUCacheCapacity),
+ WriteCache: utils.NewLruCache(mmapLRUCacheCapacity),
+ }
+
+ mmapRWManagerInstances[path] = mm
+ return mm
+
+}
+
+// MMapRWManager represents the RWManager which using mmap.
+// different with FileIORWManager, we can only allow one MMapRWManager
+// for one datafile, so we need to do some concurrency safety control.
+type MMapRWManager struct {
+ Fd *os.File
+ Path string
+ Fdm *FdManager
+ SegmentSize int64
+
+ ReadCache *utils.LRUCache
+ WriteCache *utils.LRUCache
+}
+
+// WriteAt copies data to mapped region from the b slice starting at
+// given off and returns number of bytes copied to the mapped region.
+func (mm *MMapRWManager) WriteAt(b []byte, off int64) (n int, err error) {
+ if off >= int64(mm.SegmentSize) || off < 0 {
+ return 0, ErrIndexOutOfBound
+ }
+ lb := len(b)
+ curOffset := mm.alignedOffset(off)
+ diff := off - curOffset
+ for ; n < lb && curOffset < mm.SegmentSize; curOffset += MmapBlockSize {
+ data, err := mm.accessMMap(mm.WriteCache, curOffset, mmap.RDWR)
+ if err != nil {
+ return n, err
+ }
+ data.mut.Lock()
+ n += copy(data.data[diff:MmapBlockSize], b[n:])
+ data.mut.Unlock()
+ diff = 0
+ }
+ return n, err
+}
+
+// ReadAt copies data to b slice from mapped region starting at
+// given off and returns number of bytes copied to the b slice.
+func (mm *MMapRWManager) ReadAt(b []byte, off int64) (n int, err error) {
+ if off >= int64(mm.SegmentSize) || off < 0 {
+ return 0, ErrIndexOutOfBound
+ }
+ lb := len(b)
+ curOffset := mm.alignedOffset(off)
+ diff := off - curOffset
+ for ; n < lb && curOffset < mm.SegmentSize; curOffset += MmapBlockSize {
+ data, err := mm.accessMMap(mm.ReadCache, curOffset, mmap.RDONLY)
+ if err != nil {
+ return n, err
+ }
+ data.mut.RLock()
+ n += copy(b[n:], data.data[diff:MmapBlockSize])
+ data.mut.RUnlock()
+ diff = 0
+ }
+ return n, err
+}
+
+// Sync synchronizes the mapping's contents to the file's contents on disk.
+func (mm *MMapRWManager) Sync() (err error) {
+ return nil
+}
+
+// Release deletes the memory mapped region, flushes any remaining changes
+func (mm *MMapRWManager) Release() (err error) {
+ mm.Fdm.ReduceUsing(mm.Path)
+ return nil
+}
+
+func (mm *MMapRWManager) Size() int64 {
+ return mm.SegmentSize
+}
+
+// Close will remove the cache in the fdm of the specified path, and call the close method of the os of the file
+func (mm *MMapRWManager) Close() (err error) {
+ return mm.Fdm.CloseByPath(mm.Path)
+}
+
+func (mm *MMapRWManager) alignedOffset(offset int64) int64 {
+ return offset - (offset & (MmapBlockSize - 1))
+}
+
+// accessMMap access the MMap data. If for this block the mmap data is not mmapped, will add
+// it into cache.
+func (mm *MMapRWManager) accessMMap(cache *utils.LRUCache, offset int64, prot int) (data *mmapData, err error) {
+ item := cache.Get(offset)
+ if item == nil {
+ newItem, err := newMMapData(mm.Fd, offset, prot)
+ if err != nil {
+ return nil, err
+ }
+ cache.Add(offset, newItem)
+ item = newItem
+ }
+ data = item.(*mmapData)
+ return
+}
+
+// mmapData is a struct to control the lifetime and access level of mmap.MMap
+type mmapData struct {
+ data mmap.MMap
+ offset int64
+ mut sync.RWMutex
+}
+
+func newMMapData(fd *os.File, offset int64, prot int) (md *mmapData, err error) {
+ md = &mmapData{
+ offset: offset,
+ }
+ md.data, err = mmap.MapRegion(fd, int(MmapBlockSize), prot, 0, offset)
+ if err != nil {
+ return nil, err
+ }
+ runtime.SetFinalizer(md, (*mmapData).Close)
+ return md, nil
+}
+
+func (md *mmapData) Close() (err error) {
+ return md.data.Unmap()
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/test_utils.go b/vendor/github.com/nutsdb/nutsdb/internal/testutils/test_utils.go
similarity index 61%
rename from vendor/github.com/nutsdb/nutsdb/test_utils.go
rename to vendor/github.com/nutsdb/nutsdb/internal/testutils/test_utils.go
index c1e52b6338..de0149177b 100644
--- a/vendor/github.com/nutsdb/nutsdb/test_utils.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/testutils/test_utils.go
@@ -11,17 +11,29 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package nutsdb
+package testutils
import (
- "fmt"
"math/rand"
+ "testing"
+
+ "github.com/stretchr/testify/require"
)
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func GetTestBytes(i int) []byte {
- return []byte(fmt.Sprintf("nutsdb-%09d", i))
+ // Optimized version without fmt.Sprintf to reduce benchmark overhead
+ // Format: "nutsdb-000000000" (7 prefix + 9 digits = 16 bytes)
+ buf := make([]byte, 16)
+ copy(buf, "nutsdb-")
+
+ // Convert i to 9-digit string with leading zeros
+ for j := 15; j >= 7; j-- {
+ buf[j] = byte('0' + i%10)
+ i /= 10
+ }
+ return buf
}
func GetRandomBytes(length int) []byte {
@@ -31,3 +43,11 @@ func GetRandomBytes(length int) []byte {
}
return b
}
+
+func AssertErr(t *testing.T, err error, expectErr error) {
+ if expectErr != nil {
+ require.Equal(t, expectErr, err)
+ } else {
+ require.NoError(t, err)
+ }
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/lru.go b/vendor/github.com/nutsdb/nutsdb/internal/utils/lru.go
similarity index 99%
rename from vendor/github.com/nutsdb/nutsdb/lru.go
rename to vendor/github.com/nutsdb/nutsdb/internal/utils/lru.go
index 22940aa08e..9c991ecd27 100644
--- a/vendor/github.com/nutsdb/nutsdb/lru.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/utils/lru.go
@@ -1,4 +1,4 @@
-package nutsdb
+package utils
import (
"container/list"
diff --git a/vendor/github.com/nutsdb/nutsdb/tar.go b/vendor/github.com/nutsdb/nutsdb/internal/utils/tar.go
similarity index 67%
rename from vendor/github.com/nutsdb/nutsdb/tar.go
rename to vendor/github.com/nutsdb/nutsdb/internal/utils/tar.go
index d88622aa77..dacd461568 100644
--- a/vendor/github.com/nutsdb/nutsdb/tar.go
+++ b/vendor/github.com/nutsdb/nutsdb/internal/utils/tar.go
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-package nutsdb
+package utils
import (
"archive/tar"
@@ -23,14 +23,14 @@ import (
"strings"
)
-func tarGZCompress(dst io.Writer, src string) error {
+func TarGZCompress(dst io.Writer, src string) error {
gz := gzip.NewWriter(dst)
defer gz.Close()
- return tarCompress(gz, src)
+ return TarCompress(gz, src)
}
// https://blog.ralch.com/articles/golang-working-with-tar-and-gzip
-func tarCompress(dst io.Writer, src string) error {
+func TarCompress(dst io.Writer, src string) error {
tarball := tar.NewWriter(dst)
defer tarball.Close()
@@ -77,38 +77,3 @@ func tarCompress(dst io.Writer, src string) error {
return err
})
}
-
-func tarDecompress(dst string, src io.Reader) error {
- tarReader := tar.NewReader(src)
-
- for {
- header, err := tarReader.Next()
- if err == io.EOF {
- break
- }
- if err != nil {
- return err
- }
-
- path := filepath.Join(dst, header.Name)
- info := header.FileInfo()
- if info.IsDir() {
- if err = os.MkdirAll(path, info.Mode()); err != nil {
- return err
- }
- continue
- }
-
- file, err := os.OpenFile(filepath.Clean(path), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
- if err != nil {
- return err
- }
- defer file.Close()
- _, err = io.Copy(file, tarReader)
- if err != nil {
- return err
- }
- }
-
- return nil
-}
diff --git a/vendor/github.com/nutsdb/nutsdb/internal/utils/utils.go b/vendor/github.com/nutsdb/nutsdb/internal/utils/utils.go
new file mode 100644
index 0000000000..5dce643d02
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/internal/utils/utils.go
@@ -0,0 +1,132 @@
+package utils
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "hash/fnv"
+ "io"
+ "path/filepath"
+ "reflect"
+)
+
+var fnvHash = fnv.New32a()
+
+func GetFnv32(value []byte) (uint32, error) {
+ _, err := fnvHash.Write(value)
+ if err != nil {
+ return 0, err
+ }
+ hash := fnvHash.Sum32()
+ fnvHash.Reset()
+ return hash, nil
+}
+
+func ConvertBigEndianBytesToUint64(data []byte) uint64 {
+ return binary.BigEndian.Uint64(data)
+}
+
+func ConvertUint64ToBigEndianBytes(value uint64) []byte {
+ b := make([]byte, 8)
+ binary.BigEndian.PutUint64(b, value)
+ return b
+}
+
+func MarshalInts(ints []int) ([]byte, error) {
+ buffer := bytes.NewBuffer([]byte{})
+ for _, x := range ints {
+ if err := binary.Write(buffer, binary.LittleEndian, int64(x)); err != nil {
+ return nil, err
+ }
+ }
+ return buffer.Bytes(), nil
+}
+
+func UnmarshalInts(data []byte) ([]int, error) {
+ var ints []int
+ buffer := bytes.NewBuffer(data)
+ for {
+ var i int64
+ err := binary.Read(buffer, binary.LittleEndian, &i)
+ if errors.Is(err, io.EOF) {
+ break
+ } else if err != nil {
+ return nil, err
+ }
+ ints = append(ints, int(i))
+ }
+ return ints, nil
+}
+
+func MatchForRange(pattern, bucket string, f func(bucket string) bool) (end bool, err error) {
+ match, err := filepath.Match(pattern, bucket)
+ if err != nil {
+ return true, err
+ }
+ if match && !f(bucket) {
+ return true, nil
+ }
+ return false, nil
+}
+
+func UvarintSize(x uint64) int {
+ i := 0
+ for x >= 0x80 {
+ x >>= 7
+ i++
+ }
+ return i + 1
+}
+
+func VarintSize(x int64) int {
+ ux := uint64(x<<1) ^ uint64(x>>63)
+ return UvarintSize(ux)
+}
+
+func GetDiskSizeFromSingleObject(obj interface{}) int64 {
+ typ := reflect.TypeOf(obj)
+ fields := reflect.VisibleFields(typ)
+ if len(fields) == 0 {
+ return 0
+ }
+ var size int64 = 0
+ for _, field := range fields {
+ // Currently, we only use the unsigned value type for our metadata.go. That's reasonable for us.
+ // Because it's not possible to use negative value mark the size of data.
+ // But if you want to make it more flexible, please help yourself.
+ switch field.Type.Kind() {
+ case reflect.Uint8:
+ size += 1
+ case reflect.Uint16:
+ size += 2
+ case reflect.Uint32:
+ size += 4
+ case reflect.Uint64:
+ size += 8
+ }
+ }
+ return size
+}
+
+func OneOfUint16Array(value uint16, array []uint16) bool {
+ for _, v := range array {
+ if v == value {
+ return true
+ }
+ }
+ return false
+}
+
+func EncodeListKey(key []byte, seq uint64) []byte {
+ buf := make([]byte, len(key)+8)
+ binary.LittleEndian.PutUint64(buf[:8], seq)
+ copy(buf[8:], key[:])
+ return buf
+}
+
+func DecodeListKey(buf []byte) ([]byte, uint64) {
+ seq := binary.LittleEndian.Uint64(buf[:8])
+ key := make([]byte, len(buf[8:]))
+ copy(key[:], buf[8:])
+ return key, seq
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/iterator.go b/vendor/github.com/nutsdb/nutsdb/iterator.go
index 3dcdeca748..541eeaf951 100644
--- a/vendor/github.com/nutsdb/nutsdb/iterator.go
+++ b/vendor/github.com/nutsdb/nutsdb/iterator.go
@@ -15,19 +15,26 @@
package nutsdb
import (
+ "github.com/nutsdb/nutsdb/internal/data"
"github.com/tidwall/btree"
)
type Iterator struct {
tx *Tx
options IteratorOptions
- iter btree.IterG[*Item]
+ iter btree.IterG[*data.Item[data.Record]]
+ // Cached current item to avoid repeated iter.Item() calls
+ currentItem *data.Item[data.Record]
+ // Track validity state to avoid unnecessary checks
+ valid bool
}
type IteratorOptions struct {
Reverse bool
}
+// Returns a new iterator.
+// The Release method must be called when finished with the iterator.
func NewIterator(tx *Tx, bucket string, options IteratorOptions) *Iterator {
b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
if err != nil {
@@ -36,13 +43,18 @@ func NewIterator(tx *Tx, bucket string, options IteratorOptions) *Iterator {
iterator := &Iterator{
tx: tx,
options: options,
- iter: tx.db.Index.bTree.getWithDefault(b.Id).btree.Iter(),
+ iter: tx.db.Index.bTree.getWithDefault(b.Id).Iter(),
}
+ // Initialize position and cache the first item
if options.Reverse {
- iterator.iter.Last()
+ iterator.valid = iterator.iter.Last()
} else {
- iterator.iter.First()
+ iterator.valid = iterator.iter.First()
+ }
+
+ if iterator.valid {
+ iterator.currentItem = iterator.iter.Item()
}
return iterator
@@ -50,32 +62,81 @@ func NewIterator(tx *Tx, bucket string, options IteratorOptions) *Iterator {
func (it *Iterator) Rewind() bool {
if it.options.Reverse {
- return it.iter.Last()
+ it.valid = it.iter.Last()
} else {
- return it.iter.First()
+ it.valid = it.iter.First()
}
+
+ if it.valid {
+ it.currentItem = it.iter.Item()
+ } else {
+ it.currentItem = nil
+ }
+
+ return it.valid
}
func (it *Iterator) Seek(key []byte) bool {
- return it.iter.Seek(&Item{key: key})
+ it.valid = it.iter.Seek(&data.Item[data.Record]{Key: key})
+
+ if it.valid {
+ it.currentItem = it.iter.Item()
+ } else {
+ it.currentItem = nil
+ }
+
+ return it.valid
}
func (it *Iterator) Next() bool {
+ if !it.valid {
+ return false
+ }
+
if it.options.Reverse {
- return it.iter.Prev()
+ it.valid = it.iter.Prev()
} else {
- return it.iter.Next()
+ it.valid = it.iter.Next()
}
+
+ if it.valid {
+ it.currentItem = it.iter.Item()
+ } else {
+ it.currentItem = nil
+ }
+
+ return it.valid
}
func (it *Iterator) Valid() bool {
- return it.iter.Item() != nil
+ return it.valid
}
func (it *Iterator) Key() []byte {
- return it.iter.Item().key
+ if !it.valid {
+ return nil
+ }
+ return it.currentItem.Key
}
func (it *Iterator) Value() ([]byte, error) {
- return it.tx.db.getValueByRecord(it.iter.Item().record)
+ if !it.valid {
+ return nil, ErrKeyNotFound
+ }
+ return it.tx.db.getValueByRecord(it.currentItem.Record)
+}
+
+// Item returns the current item (key + record) if valid
+// This is useful for advanced use cases that need direct access to the record
+func (it *Iterator) Item() *data.Item[data.Record] {
+ if !it.valid {
+ return nil
+ }
+ return it.currentItem
+}
+
+func (it *Iterator) Release() {
+ it.iter.Release()
+ it.currentItem = nil
+ it.valid = false
}
diff --git a/vendor/github.com/nutsdb/nutsdb/list.go b/vendor/github.com/nutsdb/nutsdb/list.go
deleted file mode 100644
index 0d474bcab5..0000000000
--- a/vendor/github.com/nutsdb/nutsdb/list.go
+++ /dev/null
@@ -1,400 +0,0 @@
-// Copyright 2023 The nutsdb Author. All rights reserved.
-//
-// Licensed 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 nutsdb
-
-import (
- "errors"
- "math"
- "time"
-)
-
-var (
- // ErrListNotFound is returned when the list not found.
- ErrListNotFound = errors.New("the list not found")
-
- // ErrCount is returned when count is error.
- ErrCount = errors.New("err count")
-
- // ErrEmptyList is returned when the list is empty.
- ErrEmptyList = errors.New("the list is empty")
-
- // ErrStartOrEnd is returned when start > end
- ErrStartOrEnd = errors.New("start or end error")
-)
-
-const (
- initialListSeq = math.MaxUint64 / 2
-)
-
-// BTree represents the btree.
-
-// HeadTailSeq list head and tail seq num
-type HeadTailSeq struct {
- Head uint64
- Tail uint64
-}
-
-// List represents the list.
-type List struct {
- Items map[string]*BTree
- TTL map[string]uint32
- TimeStamp map[string]uint64
- Seq map[string]*HeadTailSeq
-}
-
-func NewList() *List {
- return &List{
- Items: make(map[string]*BTree),
- TTL: make(map[string]uint32),
- TimeStamp: make(map[string]uint64),
- Seq: make(map[string]*HeadTailSeq),
- }
-}
-
-func (l *List) LPush(key string, r *Record) error {
- return l.push(key, r, true)
-}
-
-func (l *List) RPush(key string, r *Record) error {
- return l.push(key, r, false)
-}
-
-func (l *List) push(key string, r *Record, isLeft bool) error {
- // key is seq + user_key
- userKey, curSeq := decodeListKey([]byte(key))
- userKeyStr := string(userKey)
- if l.IsExpire(userKeyStr) {
- return ErrListNotFound
- }
-
- list, ok := l.Items[userKeyStr]
- if !ok {
- l.Items[userKeyStr] = NewBTree()
- list = l.Items[userKeyStr]
- }
-
- seq, ok := l.Seq[userKeyStr]
- if !ok {
- l.Seq[userKeyStr] = &HeadTailSeq{Head: initialListSeq, Tail: initialListSeq + 1}
- seq = l.Seq[userKeyStr]
- }
-
- list.InsertRecord(ConvertUint64ToBigEndianBytes(curSeq), r)
- if isLeft {
- if seq.Head > curSeq-1 {
- seq.Head = curSeq - 1
- }
- } else {
- if seq.Tail < curSeq+1 {
- seq.Tail = curSeq + 1
- }
- }
-
- return nil
-}
-
-func (l *List) LPop(key string) (*Record, error) {
- item, err := l.LPeek(key)
- if err != nil {
- return nil, err
- }
-
- l.Items[key].Delete(item.key)
- l.Seq[key].Head = ConvertBigEndianBytesToUint64(item.key)
- return item.record, nil
-}
-
-// RPop removes and returns the last element of the list stored at key.
-func (l *List) RPop(key string) (*Record, error) {
- item, err := l.RPeek(key)
- if err != nil {
- return nil, err
- }
-
- l.Items[key].Delete(item.key)
- l.Seq[key].Tail = ConvertBigEndianBytesToUint64(item.key)
- return item.record, nil
-}
-
-func (l *List) LPeek(key string) (*Item, error) {
- return l.peek(key, true)
-}
-
-func (l *List) RPeek(key string) (*Item, error) {
- return l.peek(key, false)
-}
-
-func (l *List) peek(key string, isLeft bool) (*Item, error) {
- if l.IsExpire(key) {
- return nil, ErrListNotFound
- }
- list, ok := l.Items[key]
- if !ok {
- return nil, ErrListNotFound
- }
-
- if isLeft {
- item, ok := list.Min()
- if ok {
- return item, nil
- }
- } else {
- item, ok := list.Max()
- if ok {
- return item, nil
- }
- }
-
- return nil, ErrEmptyList
-}
-
-// LRange returns the specified elements of the list stored at key [start,end]
-func (l *List) LRange(key string, start, end int) ([]*Record, error) {
- size, err := l.Size(key)
- if err != nil || size == 0 {
- return nil, err
- }
-
- start, end, err = checkBounds(start, end, size)
- if err != nil {
- return nil, err
- }
-
- var res []*Record
- allRecords := l.Items[key].All()
- for i, item := range allRecords {
- if i >= start && i <= end {
- res = append(res, item)
- }
- }
-
- return res, nil
-}
-
-// getRemoveIndexes returns a slice of indices to be removed from the list based on the count
-func (l *List) getRemoveIndexes(key string, count int, cmp func(r *Record) (bool, error)) ([][]byte, error) {
- if l.IsExpire(key) {
- return nil, ErrListNotFound
- }
-
- list, ok := l.Items[key]
-
- if !ok {
- return nil, ErrListNotFound
- }
-
- var res [][]byte
- var allItems []*Item
- if 0 == count {
- count = list.Count()
- }
-
- allItems = l.Items[key].AllItems()
- if count > 0 {
- for _, item := range allItems {
- if count <= 0 {
- break
- }
- r := item.record
- ok, err := cmp(r)
- if err != nil {
- return nil, err
- }
- if ok {
- res = append(res, item.key)
- count--
- }
- }
- } else {
- for i := len(allItems) - 1; i >= 0; i-- {
- if count >= 0 {
- break
- }
- r := allItems[i].record
- ok, err := cmp(r)
- if err != nil {
- return nil, err
- }
- if ok {
- res = append(res, allItems[i].key)
- count++
- }
- }
- }
-
- return res, nil
-}
-
-// LRem removes the first count occurrences of elements equal to value from the list stored at key.
-// The count argument influences the operation in the following ways:
-// count > 0: Remove elements equal to value moving from head to tail.
-// count < 0: Remove elements equal to value moving from tail to head.
-// count = 0: Remove all elements equal to value.
-func (l *List) LRem(key string, count int, cmp func(r *Record) (bool, error)) error {
- removeIndexes, err := l.getRemoveIndexes(key, count, cmp)
- if err != nil {
- return err
- }
-
- list := l.Items[key]
- for _, idx := range removeIndexes {
- list.Delete(idx)
- }
-
- return nil
-}
-
-// LTrim trim an existing list so that it will contain only the specified range of elements specified.
-func (l *List) LTrim(key string, start, end int) error {
- if l.IsExpire(key) {
- return ErrListNotFound
- }
- if _, ok := l.Items[key]; !ok {
- return ErrListNotFound
- }
-
- list := l.Items[key]
- allItems := list.AllItems()
- for i, item := range allItems {
- if i < start || i > end {
- list.Delete(item.key)
- }
- }
-
- return nil
-}
-
-// LRemByIndex remove the list element at specified index
-func (l *List) LRemByIndex(key string, indexes []int) error {
- if l.IsExpire(key) {
- return ErrListNotFound
- }
-
- idxes := l.getValidIndexes(key, indexes)
- if len(idxes) == 0 {
- return nil
- }
-
- list := l.Items[key]
- allItems := list.AllItems()
- for i, item := range allItems {
- if _, ok := idxes[i]; ok {
- list.Delete(item.key)
- }
- }
-
- return nil
-}
-
-func (l *List) getValidIndexes(key string, indexes []int) map[int]struct{} {
- idxes := make(map[int]struct{})
- listLen, err := l.Size(key)
- if err != nil || 0 == listLen {
- return idxes
- }
-
- for _, idx := range indexes {
- if idx < 0 || idx >= listLen {
- continue
- }
- idxes[idx] = struct{}{}
- }
-
- return idxes
-}
-
-func (l *List) IsExpire(key string) bool {
- if l == nil {
- return false
- }
-
- _, ok := l.TTL[key]
- if !ok {
- return false
- }
-
- now := time.Now().Unix()
- timestamp := l.TimeStamp[key]
- if l.TTL[key] > 0 && uint64(l.TTL[key])+timestamp > uint64(now) || l.TTL[key] == uint32(0) {
- return false
- }
-
- delete(l.Items, key)
- delete(l.TTL, key)
- delete(l.TimeStamp, key)
- delete(l.Seq, key)
-
- return true
-}
-
-func (l *List) Size(key string) (int, error) {
- if l.IsExpire(key) {
- return 0, ErrListNotFound
- }
- if _, ok := l.Items[key]; !ok {
- return 0, ErrListNotFound
- }
-
- return l.Items[key].Count(), nil
-}
-
-func (l *List) IsEmpty(key string) (bool, error) {
- size, err := l.Size(key)
- if err != nil || size > 0 {
- return false, err
- }
- return true, nil
-}
-
-func (l *List) GetListTTL(key string) (uint32, error) {
- if l.IsExpire(key) {
- return 0, ErrListNotFound
- }
-
- ttl := l.TTL[key]
- timestamp := l.TimeStamp[key]
- if ttl == 0 || timestamp == 0 {
- return 0, nil
- }
-
- now := time.Now().Unix()
- remain := timestamp + uint64(ttl) - uint64(now)
-
- return uint32(remain), nil
-}
-
-func checkBounds(start, end int, size int) (int, int, error) {
- if start >= 0 && end < 0 {
- end = size + end
- }
-
- if start < 0 && end > 0 {
- start = size + start
- }
-
- if start < 0 && end < 0 {
- start, end = size+start, size+end
- }
-
- if end >= size {
- end = size - 1
- }
-
- if start > end {
- return 0, 0, ErrStartOrEnd
- }
-
- return start, end, nil
-}
diff --git a/vendor/github.com/nutsdb/nutsdb/merge.go b/vendor/github.com/nutsdb/nutsdb/merge.go
index a75711ce9e..a45653097f 100644
--- a/vendor/github.com/nutsdb/nutsdb/merge.go
+++ b/vendor/github.com/nutsdb/nutsdb/merge.go
@@ -23,16 +23,24 @@ import (
"os"
"time"
+ "github.com/nutsdb/nutsdb/internal/utils"
"github.com/xujiajun/utils/strconv2"
)
-var ErrDontNeedMerge = errors.New("the number of files waiting to be merged is at least 2")
+var ErrDontNeedMerge = errors.New("the number of files waiting to be merged is less than 2")
func (db *DB) Merge() error {
db.mergeStartCh <- struct{}{}
return <-db.mergeEndCh
}
+func (db *DB) merge() error {
+ if db.opt.EnableMergeV2 {
+ return db.mergeV2()
+ }
+ return db.mergeLegacy()
+}
+
// Merge removes dirty data and reduce data redundancy,following these steps:
//
// 1. Filter delete or expired entry.
@@ -45,10 +53,10 @@ func (db *DB) Merge() error {
//
// Caveat: merge is Called means starting multiple write transactions, and it
// will affect the other write request. so execute it at the appropriate time.
-func (db *DB) merge() error {
+func (db *DB) mergeLegacy() error {
var (
off int64
- pendingMergeFIds []int
+ pendingMergeFIds []int64
)
// to prevent the initiation of multiple merges simultaneously.
@@ -86,21 +94,25 @@ func (db *DB) merge() error {
var err error
path := getDataPath(db.MaxFileID, db.opt.Dir)
- db.ActiveFile, err = db.fm.getDataFile(path, db.opt.SegmentSize)
+ db.ActiveFile, err = db.fm.GetDataFile(path, db.opt.SegmentSize)
if err != nil {
db.mu.Unlock()
return err
}
db.ActiveFile.fileID = db.MaxFileID
+ startFileID := db.MaxFileID
db.mu.Unlock()
mergingPath := make([]string, len(pendingMergeFIds))
+ // Used to collect all merged entry information for later writing to the HintFile
+ var mergedEntries []mergedEntryInfo
+
for i, pendingMergeFId := range pendingMergeFIds {
off = 0
- path := getDataPath(int64(pendingMergeFId), db.opt.Dir)
+ path := getDataPath(pendingMergeFId, db.opt.Dir)
fr, err := newFileRecovery(path, db.opt.BufferSizeOfRecovery)
if err != nil {
return err
@@ -132,23 +144,35 @@ func (db *DB) merge() error {
return err
}
bucketName := bucket.Name
- if entry.Meta.Flag == DataLPushFlag {
- return tx.LPushRaw(bucketName, entry.Key, entry.Value)
- }
- if entry.Meta.Flag == DataRPushFlag {
- return tx.RPushRaw(bucketName, entry.Key, entry.Value)
+ switch entry.Meta.Flag {
+ case DataLPushFlag:
+ if err := tx.LPushRaw(bucketName, entry.Key, entry.Value); err != nil {
+ return err
+ }
+ case DataRPushFlag:
+ if err := tx.RPushRaw(bucketName, entry.Key, entry.Value); err != nil {
+ return err
+ }
+ default:
+ if err := tx.put(
+ bucketName,
+ entry.Key,
+ entry.Value,
+ entry.Meta.TTL,
+ entry.Meta.Flag,
+ entry.Meta.Timestamp,
+ entry.Meta.Ds,
+ ); err != nil {
+ return err
+ }
}
- return tx.put(
- bucketName,
- entry.Key,
- entry.Value,
- entry.Meta.TTL,
- entry.Meta.Flag,
- entry.Meta.Timestamp,
- entry.Meta.Ds,
- )
+ // Record merged entry information to get actual position from index later
+ mergedEntries = append(mergedEntries, mergedEntryInfo{
+ entry: entry,
+ bucketId: bucket.Id,
+ })
}
return nil
@@ -162,7 +186,6 @@ func (db *DB) merge() error {
if off >= db.opt.SegmentSize {
break
}
-
} else {
if errors.Is(err, io.EOF) || errors.Is(err, ErrIndexOutOfBound) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, ErrHeaderSizeOutOfBounds) {
break
@@ -178,6 +201,19 @@ func (db *DB) merge() error {
mergingPath[i] = path
}
+ // Now that all data has been written, create HintFile for all newly generated data files
+ // Only create HintFile for new files actually generated during the Merge process
+ db.mu.Lock()
+ endFileID := db.MaxFileID // Record the maximum file ID when merge ends
+ db.mu.Unlock()
+
+ // 只有当启用 HintFile 功能时才创建 HintFile
+ if db.opt.EnableHintFile {
+ if err := db.buildHintFilesAfterMerge(startFileID, endFileID); err != nil {
+ return fmt.Errorf("failed to build hint files after merge: %w", err)
+ }
+ }
+
db.mu.Lock()
defer db.mu.Unlock()
@@ -185,6 +221,91 @@ func (db *DB) merge() error {
if err := os.Remove(mergingPath[i]); err != nil {
return fmt.Errorf("when merge err: %s", err)
}
+
+ // Delete the HintFile corresponding to the old data file (if it exists)
+ oldHintPath := getHintPath(int64(pendingMergeFIds[i]), db.opt.Dir)
+ if _, err := os.Stat(oldHintPath); err == nil {
+ if removeErr := os.Remove(oldHintPath); removeErr != nil {
+ // Log error but don't interrupt the merge process
+ fmt.Printf("warning: failed to remove old hint file %s: %v\n", oldHintPath, removeErr)
+ }
+ }
+ }
+
+ return nil
+}
+
+// buildHintFilesAfterMerge creates HintFiles for all newly generated data files after merge
+// by traversing the index to find all records pointing to new files.
+// Only process files in the range [startFileID, endFileID]
+func (db *DB) buildHintFilesAfterMerge(startFileID, endFileID int64) error {
+ if startFileID > endFileID {
+ return nil
+ }
+
+ for fileID := startFileID; fileID <= endFileID; fileID++ {
+ dataPath := getDataPath(fileID, db.opt.Dir)
+ fr, err := newFileRecovery(dataPath, db.opt.BufferSizeOfRecovery)
+ if err != nil {
+ return fmt.Errorf("failed to open data file %s: %w", dataPath, err)
+ }
+
+ hintPath := getHintPath(fileID, db.opt.Dir)
+ hintWriter := &HintFileWriter{}
+ if err := hintWriter.Create(hintPath); err != nil {
+ _ = fr.release()
+ return fmt.Errorf("failed to create hint file %s: %w", hintPath, err)
+ }
+
+ cleanup := func(remove bool) {
+ _ = hintWriter.Close()
+ _ = fr.release()
+ if remove {
+ _ = os.Remove(hintPath)
+ }
+ }
+
+ off := int64(0)
+ for {
+ entry, err := fr.readEntry(off)
+ if err != nil {
+ if errors.Is(err, io.EOF) || errors.Is(err, ErrIndexOutOfBound) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, ErrHeaderSizeOutOfBounds) {
+ break
+ }
+ cleanup(true)
+ return fmt.Errorf("failed to read entry from %s at offset %d: %w", dataPath, off, err)
+ }
+
+ if entry == nil {
+ break
+ }
+
+ hintEntry := newHintEntryFromEntry(entry, fileID, uint64(off))
+
+ if err := hintWriter.Write(hintEntry); err != nil {
+ cleanup(true)
+ return fmt.Errorf("failed to write hint entry to %s: %w", hintPath, err)
+ }
+
+ off += entry.Size()
+ if off >= fr.size {
+ break
+ }
+ }
+
+ if err := hintWriter.Sync(); err != nil {
+ cleanup(true)
+ return fmt.Errorf("failed to sync hint file %s: %w", hintPath, err)
+ }
+
+ if err := hintWriter.Close(); err != nil {
+ _ = fr.release()
+ return fmt.Errorf("failed to close hint file %s: %w", hintPath, err)
+ }
+
+ if err := fr.release(); err != nil {
+ return fmt.Errorf("failed to close data file %s: %w", dataPath, err)
+ }
}
return nil
@@ -218,26 +339,21 @@ func (db *DB) mergeWorker() {
}
func (db *DB) isPendingMergeEntry(entry *Entry) bool {
- bucket, err := db.bm.GetBucketById(entry.Meta.BucketId)
- if err != nil {
- return false
- }
- bucketId := bucket.Id
switch {
- case entry.IsBelongsToBPlusTree():
- return db.isPendingBtreeEntry(bucketId, entry)
+ case entry.IsBelongsToBTree():
+ return db.isPendingBtreeEntry(entry)
case entry.IsBelongsToList():
- return db.isPendingListEntry(bucketId, entry)
+ return db.isPendingListEntry(entry)
case entry.IsBelongsToSet():
- return db.isPendingSetEntry(bucketId, entry)
+ return db.isPendingSetEntry(entry)
case entry.IsBelongsToSortSet():
- return db.isPendingZSetEntry(bucketId, entry)
+ return db.isPendingZSetEntry(entry)
}
return false
}
-func (db *DB) isPendingBtreeEntry(bucketId BucketId, entry *Entry) bool {
- idx, exist := db.Index.bTree.exist(bucketId)
+func (db *DB) isPendingBtreeEntry(entry *Entry) bool {
+ idx, exist := db.Index.bTree.exist(entry.Meta.BucketId)
if !exist {
return false
}
@@ -248,7 +364,7 @@ func (db *DB) isPendingBtreeEntry(bucketId BucketId, entry *Entry) bool {
}
if r.IsExpired() {
- db.tm.del(bucketId, string(entry.Key))
+ db.tm.del(entry.Meta.BucketId, string(entry.Key))
idx.Delete(entry.Key)
return false
}
@@ -260,8 +376,8 @@ func (db *DB) isPendingBtreeEntry(bucketId BucketId, entry *Entry) bool {
return true
}
-func (db *DB) isPendingSetEntry(bucketId BucketId, entry *Entry) bool {
- setIdx, exist := db.Index.set.exist(bucketId)
+func (db *DB) isPendingSetEntry(entry *Entry) bool {
+ setIdx, exist := db.Index.set.exist(entry.Meta.BucketId)
if !exist {
return false
}
@@ -274,9 +390,9 @@ func (db *DB) isPendingSetEntry(bucketId BucketId, entry *Entry) bool {
return true
}
-func (db *DB) isPendingZSetEntry(bucketId BucketId, entry *Entry) bool {
+func (db *DB) isPendingZSetEntry(entry *Entry) bool {
key, score := splitStringFloat64Str(string(entry.Key), SeparatorForZSetKey)
- sortedSetIdx, exist := db.Index.sortedSet.exist(bucketId)
+ sortedSetIdx, exist := db.Index.sortedSet.exist(entry.Meta.BucketId)
if !exist {
return false
}
@@ -288,14 +404,14 @@ func (db *DB) isPendingZSetEntry(bucketId BucketId, entry *Entry) bool {
return true
}
-func (db *DB) isPendingListEntry(bucketId BucketId, entry *Entry) bool {
+func (db *DB) isPendingListEntry(entry *Entry) bool {
var userKeyStr string
var curSeq uint64
var userKey []byte
if entry.Meta.Flag == DataExpireListFlag {
userKeyStr = string(entry.Key)
- list, exist := db.Index.list.exist(bucketId)
+ list, exist := db.Index.list.exist(entry.Meta.BucketId)
if !exist {
return false
}
@@ -321,7 +437,7 @@ func (db *DB) isPendingListEntry(bucketId BucketId, entry *Entry) bool {
userKey, curSeq = decodeListKey(entry.Key)
userKeyStr = string(userKey)
- list, exist := db.Index.list.exist(bucketId)
+ list, exist := db.Index.list.exist(entry.Meta.BucketId)
if !exist {
return false
}
@@ -330,7 +446,7 @@ func (db *DB) isPendingListEntry(bucketId BucketId, entry *Entry) bool {
return false
}
- r, ok := list.Items[userKeyStr].Find(ConvertUint64ToBigEndianBytes(curSeq))
+ r, ok := list.Items[userKeyStr].Find(utils.ConvertUint64ToBigEndianBytes(curSeq))
if !ok {
return false
}
@@ -344,3 +460,9 @@ func (db *DB) isPendingListEntry(bucketId BucketId, entry *Entry) bool {
return false
}
+
+// mergedEntryInfo 用于在 merge 过程中暂存条目信息
+type mergedEntryInfo struct {
+ entry *Entry
+ bucketId BucketId
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/merge_manifest.go b/vendor/github.com/nutsdb/nutsdb/merge_manifest.go
new file mode 100644
index 0000000000..db7dd6b9aa
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/merge_manifest.go
@@ -0,0 +1,65 @@
+package nutsdb
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+type mergeManifestStatus string
+
+const (
+ mergeManifestFileName = "merge_manifest.json"
+ mergeManifestTempFileSuffix = ".tmp"
+
+ manifestStatusWriting mergeManifestStatus = "writing"
+ manifestStatusCommitted mergeManifestStatus = "committed"
+)
+
+type mergeManifest struct {
+ Status mergeManifestStatus `json:"status"`
+ MergeSeqMax int `json:"mergeSeqMax"`
+ PendingOldFileIDs []int64 `json:"pendingOldFileIDs"`
+}
+
+func loadMergeManifest(dir string) (*mergeManifest, error) {
+ path := filepath.Join(dir, mergeManifestFileName)
+ data, err := os.ReadFile(path)
+ if errors.Is(err, os.ErrNotExist) {
+ return nil, nil
+ }
+ if err != nil {
+ return nil, fmt.Errorf("read merge manifest: %w", err)
+ }
+ var manifest mergeManifest
+ if err := json.Unmarshal(data, &manifest); err != nil {
+ return nil, fmt.Errorf("decode merge manifest: %w", err)
+ }
+ return &manifest, nil
+}
+
+func writeMergeManifest(dir string, manifest *mergeManifest) error {
+ data, err := json.Marshal(manifest)
+ if err != nil {
+ return fmt.Errorf("encode merge manifest: %w", err)
+ }
+ manifestPath := filepath.Join(dir, mergeManifestFileName)
+ tmpPath := manifestPath + mergeManifestTempFileSuffix
+ if err := os.WriteFile(tmpPath, data, 0o644); err != nil {
+ return fmt.Errorf("write manifest tmp: %w", err)
+ }
+ if err := os.Rename(tmpPath, manifestPath); err != nil {
+ return fmt.Errorf("rename manifest tmp: %w", err)
+ }
+ return nil
+}
+
+func removeMergeManifest(dir string) error {
+ path := filepath.Join(dir, mergeManifestFileName)
+ if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("remove merge manifest: %w", err)
+ }
+ return nil
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/merge_recovery.go b/vendor/github.com/nutsdb/nutsdb/merge_recovery.go
new file mode 100644
index 0000000000..8c836e4635
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/merge_recovery.go
@@ -0,0 +1,50 @@
+package nutsdb
+
+import (
+ "errors"
+ "fmt"
+ "os"
+)
+
+func (db *DB) recoverMergeManifest() error {
+ manifest, err := loadMergeManifest(db.opt.Dir)
+ if err != nil {
+ return err
+ }
+ if manifest == nil {
+ return nil
+ }
+
+ switch manifest.Status {
+ case manifestStatusWriting:
+ keep := make(map[int64]struct{}, len(manifest.PendingOldFileIDs))
+ for _, fid := range manifest.PendingOldFileIDs {
+ if !IsMergeFile(fid) {
+ continue
+ }
+ keep[fid] = struct{}{}
+ }
+ if err := purgeMergeFiles(db.opt.Dir, keep); err != nil {
+ return fmt.Errorf("purge stale merge files: %w", err)
+ }
+ if err := removeMergeManifest(db.opt.Dir); err != nil {
+ return err
+ }
+ case manifestStatusCommitted:
+ for _, fid := range manifest.PendingOldFileIDs {
+ if err := os.Remove(getDataPath(fid, db.opt.Dir)); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("remove old data file %d: %w", fid, err)
+ }
+ if err := os.Remove(getHintPath(fid, db.opt.Dir)); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return fmt.Errorf("remove old hint file %d: %w", fid, err)
+ }
+ }
+ if err := removeMergeManifest(db.opt.Dir); err != nil {
+ return err
+ }
+ default:
+ return fmt.Errorf("unknown merge manifest status %q", manifest.Status)
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/merge_utils.go b/vendor/github.com/nutsdb/nutsdb/merge_utils.go
new file mode 100644
index 0000000000..a2a8323e3c
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/merge_utils.go
@@ -0,0 +1,111 @@
+package nutsdb
+
+import (
+ "fmt"
+ "math"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+const MergeFileIDBase int64 = math.MinInt64
+
+func GetMergeFileID(seq int) int64 {
+ return MergeFileIDBase + int64(seq)
+}
+
+func IsMergeFile(fileID int64) bool {
+ return fileID < 0
+}
+
+func GetMergeSeq(fileID int64) int {
+ return int(fileID - MergeFileIDBase)
+}
+
+func mergeFilePrefix() string {
+ return "merge_"
+}
+
+func mergeDataFileName(seq int) string {
+ return fmt.Sprintf("%s%d%s", mergeFilePrefix(), seq, DataSuffix)
+}
+
+func mergeHintFileName(seq int) string {
+ return fmt.Sprintf("%s%d%s", mergeFilePrefix(), seq, HintSuffix)
+}
+
+func getMergeDataPath(dir string, seq int) string {
+ return filepath.Join(dir, mergeDataFileName(seq))
+}
+
+func getMergeHintPath(dir string, seq int) string {
+ return filepath.Join(dir, mergeHintFileName(seq))
+}
+
+func parseMergeSeq(name string) (int, bool) {
+ if !strings.HasPrefix(name, mergeFilePrefix()) {
+ return 0, false
+ }
+ suffix := strings.TrimPrefix(name, mergeFilePrefix())
+ idx := strings.Index(suffix, ".")
+ if idx > 0 {
+ suffix = suffix[:idx]
+ }
+ seq, err := strconv.Atoi(suffix)
+ if err != nil {
+ return 0, false
+ }
+ return seq, true
+}
+
+func enumerateDataFileIDs(dir string) (userIDs []int64, mergeIDs []int64, err error) {
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return nil, nil, err
+ }
+ for _, entry := range entries {
+ if entry.IsDir() {
+ continue
+ }
+ name := entry.Name()
+ if !strings.HasSuffix(name, DataSuffix) {
+ continue
+ }
+ base := strings.TrimSuffix(name, DataSuffix)
+ if seq, ok := parseMergeSeq(base); ok {
+ mergeIDs = append(mergeIDs, GetMergeFileID(seq))
+ continue
+ }
+ id, err := strconv.ParseInt(base, 10, 64)
+ if err != nil {
+ continue
+ }
+ userIDs = append(userIDs, id)
+ }
+ sort.Slice(userIDs, func(i, j int) bool { return userIDs[i] < userIDs[j] })
+ sort.Slice(mergeIDs, func(i, j int) bool { return mergeIDs[i] < mergeIDs[j] })
+ return userIDs, mergeIDs, nil
+}
+
+func purgeMergeFiles(dir string, keep map[int64]struct{}) error {
+ _, mergeIDs, err := enumerateDataFileIDs(dir)
+ if err != nil {
+ return err
+ }
+ for _, fid := range mergeIDs {
+ if keep != nil {
+ if _, ok := keep[fid]; ok {
+ continue
+ }
+ }
+ if err := os.Remove(getDataPath(fid, dir)); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ if err := os.Remove(getHintPath(fid, dir)); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/merge_v2.go b/vendor/github.com/nutsdb/nutsdb/merge_v2.go
new file mode 100644
index 0000000000..827dd8e131
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/merge_v2.go
@@ -0,0 +1,726 @@
+package nutsdb
+
+import (
+ "errors"
+ "fmt"
+ "hash"
+ "hash/fnv"
+ "io"
+ "os"
+ "sort"
+
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/utils"
+)
+
+// mergeV2Job manages the entire merge operation lifecycle.
+//
+// # Memory Efficiency Design
+//
+// This implementation prioritizes memory efficiency over runtime validation by eliminating
+// staleness checking during the commit phase. In large-scale merges (e.g., 10GB+ of data),
+// this design choice provides significant memory savings:
+//
+// - Previous approach: ~145 bytes per entry (with staleness metadata)
+// - Current approach: ~50 bytes per entry (minimal metadata)
+// - Savings: ~65% memory reduction for 10M entries (~1.4GB → ~500MB)
+//
+// # Correctness Guarantee via Index Rebuild
+//
+// Stale entries (those updated concurrently during merge) may be written to hint files.
+// This is safe because of the file ordering guarantee during index rebuild:
+//
+// 1. Merge files use negative FileIDs (starting from math.MinInt64)
+// 2. Normal data files use positive FileIDs (starting from 0)
+// 3. Index rebuild processes files in ascending FileID order
+// 4. Therefore: merge files are always processed BEFORE normal files
+//
+// Example scenario:
+// - Merge begins: entry "foo" at File#3, timestamp=1000
+// - During merge: concurrent update writes "foo" to File#4, timestamp=2000
+// - Merge writes stale version to MergeFile#-9223372036854775808
+// - On restart/rebuild:
+// Step 1: Load MergeFile (fileID=-9223372036854775808) → index["foo"] = {ts:1000, stale}
+// Step 2: Load File#3 → skipped (merged away)
+// Step 3: Load File#4 → index["foo"] = {ts:2000, fresh} ✓ Overwrites stale value
+//
+// This design trades hint file size and rebuild time for dramatically reduced memory usage
+// during merge operations, which is critical for Bitcask-based systems that already
+// consume significant memory for in-memory indexes.
+type mergeV2Job struct {
+ db *DB
+ pending []int64
+ outputs []*mergeOutput
+ lookup []*mergeLookupEntry
+ manifest *mergeManifest
+ oldData []string
+ oldHints []string
+ outputSeqBase int
+ valueHasher hash.Hash32
+ onRewriteEntry func(*Entry)
+}
+
+// mergeLookupEntry tracks minimal information needed to update indexes at commit time.
+// We no longer store original file metadata for staleness checking - this reduces memory usage
+// significantly in large merges (from ~145 bytes/entry to ~50 bytes/entry + key length).
+type mergeLookupEntry struct {
+ hint *HintEntry // Hint entry containing key and location metadata
+ valueHash uint32 // Hash of the value for Set/SortedSet duplicate detection
+ hasValueHash bool // Indicates if valueHash is valid
+ collector *HintCollector // Hint file collector for writing hints
+}
+
+// mergeOutput represents a single merge output file with its associated hint file.
+// Each merge operation may produce multiple output files to respect segment size limits.
+type mergeOutput struct {
+ seq int // Sequence number within this merge operation
+ fileID int64 // File ID (negative for merge files)
+ dataFile *DataFile // Output data file handle
+ collector *HintCollector // Hint file collector for this output
+ dataPath string // Path to the data file
+ hintPath string // Path to the hint file
+ writeOff int64 // Current write offset in the data file
+ finalized bool // Whether this output has been finalized
+}
+
+// mergeV2 executes the complete merge operation using the V2 algorithm.
+// This is the main entry point for the merge process, orchestrating all phases.
+func (db *DB) mergeV2() error {
+ job := &mergeV2Job{db: db}
+
+ // Prepare merge job - validate state and enumerate files
+ if err := job.prepare(); err != nil {
+ return err
+ }
+ defer job.finish()
+
+ // Enter writing state - prepare for merge operations
+ if err := job.enterWritingState(); err != nil {
+ return job.abort(err)
+ }
+
+ // Rewrite phase - process all pending files and create merge outputs
+ if err := job.rewrite(); err != nil {
+ return job.abort(err)
+ }
+
+ // Commit phase - update indexes and write hint files
+ if err := job.commit(); err != nil {
+ return job.abort(err)
+ }
+
+ // Finalize outputs - ensure all data is persisted
+ if err := job.finalizeOutputs(); err != nil {
+ return job.abort(err)
+ }
+
+ // Clean up old files to reclaim disk space
+ if err := job.cleanupOldFiles(); err != nil {
+ return fmt.Errorf("cleanup old files: %w", err)
+ }
+
+ return nil
+}
+
+// prepare initializes the merge job by validating state, enumerating files, and setting up the database.
+// It ensures the database is ready for merge and creates a new active file for ongoing writes.
+func (job *mergeV2Job) prepare() error {
+ job.db.mu.Lock()
+
+ // Prevent concurrent merges
+ if job.db.isMerging {
+ job.db.mu.Unlock()
+ return ErrIsMerging
+ }
+
+ // Enumerate all data files (both user and merge files)
+ userIDs, mergeIDs, err := enumerateDataFileIDs(job.db.opt.Dir)
+ if err != nil {
+ job.db.mu.Unlock()
+ return fmt.Errorf("failed to enumerate data file IDs: %w", err)
+ }
+
+ // Collect all pending files for merging
+ job.pending = append(job.pending, userIDs...)
+ job.pending = append(job.pending, mergeIDs...)
+
+ // Determine next merge sequence number based on existing merge files
+ maxSeq := -1
+ for _, fid := range mergeIDs {
+ seq := GetMergeSeq(fid)
+ if seq > maxSeq {
+ maxSeq = seq
+ }
+ }
+ if maxSeq >= 0 {
+ job.outputSeqBase = maxSeq + 1
+ }
+
+ // Skip merge if there are fewer than 2 files
+ if len(job.pending) < 2 {
+ job.db.mu.Unlock()
+ return ErrDontNeedMerge
+ }
+
+ // Sort files by ID for consistent processing order
+ sort.Slice(job.pending, func(i, j int) bool { return job.pending[i] < job.pending[j] })
+
+ // Mark database as merging
+ job.db.isMerging = true
+
+ // Sync active file if using mmap without sync
+ if !job.db.opt.SyncEnable && job.db.opt.RWMode == MMap {
+ if err := job.db.ActiveFile.rwManager.Sync(); err != nil {
+ job.db.isMerging = false
+ job.db.mu.Unlock()
+ return fmt.Errorf("failed to sync active file: %w", err)
+ }
+ }
+
+ // Release current active file for merge processing
+ if err := job.db.ActiveFile.rwManager.Release(); err != nil {
+ job.db.isMerging = false
+ job.db.mu.Unlock()
+ return fmt.Errorf("failed to release active file: %w", err)
+ }
+
+ // Create new active file for writes during merge
+ job.db.MaxFileID++
+ path := getDataPath(job.db.MaxFileID, job.db.opt.Dir)
+ activeFile, err := job.db.fm.GetDataFile(path, job.db.opt.SegmentSize)
+ if err != nil {
+ job.db.isMerging = false
+ job.db.mu.Unlock()
+ return fmt.Errorf("failed to create new active file: %w", err)
+ }
+ job.db.ActiveFile = activeFile
+ job.db.ActiveFile.fileID = job.db.MaxFileID
+
+ job.db.mu.Unlock()
+
+ return nil
+}
+
+// finish cleans up the merge job by resetting the merging state.
+// Always called via defer to ensure cleanup even if merge fails.
+func (job *mergeV2Job) finish() {
+ job.db.mu.Lock()
+ job.db.isMerging = false
+ job.db.mu.Unlock()
+}
+
+// enterWritingState initializes the merge job for writing entries.
+// Creates the merge manifest and prepares necessary data structures.
+func (job *mergeV2Job) enterWritingState() error {
+ // Initialize lookup entries for tracking merged entries
+ job.lookup = make([]*mergeLookupEntry, 0)
+
+ // Create merge manifest to track merge progress
+ job.manifest = &mergeManifest{
+ Status: manifestStatusWriting,
+ MergeSeqMax: -1,
+ PendingOldFileIDs: append([]int64(nil), job.pending...),
+ }
+
+ // Initialize value hasher for Set/SortedSet duplicate detection
+ if job.valueHasher == nil {
+ job.valueHasher = fnv.New32a()
+ }
+
+ // Write initial manifest to enable recovery
+ if err := writeMergeManifest(job.db.opt.Dir, job.manifest); err != nil {
+ return err
+ }
+ return nil
+}
+
+// rewrite processes all pending files and rewrites their valid entries to merge outputs.
+// This is the main phase where data compaction happens.
+func (job *mergeV2Job) rewrite() error {
+ for _, fid := range job.pending {
+ if err := job.rewriteFile(fid); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// finalizeOutputs ensures all output files are properly closed and synced to disk.
+// This must be called after all entries have been written.
+func (job *mergeV2Job) finalizeOutputs() error {
+ for _, out := range job.outputs {
+ if err := out.finalize(); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// commit atomically updates in-memory indexes and writes hint files.
+// Note: We don't validate staleness here. If an entry was updated concurrently during merge,
+// we'll write the stale version to the hint file. This is safe because during index rebuild,
+// merge files (negative FileIDs) are processed before normal files (positive FileIDs), so
+// newer values will overwrite stale ones. This tradeoff saves significant memory.
+func (job *mergeV2Job) commit() error {
+ job.db.mu.Lock()
+ defer job.db.mu.Unlock()
+
+ // Phase 1: Write all hints to hint files (even potentially stale ones)
+ // This ensures hints are available for fast index recovery
+ for _, entry := range job.lookup {
+ if entry == nil {
+ continue
+ }
+ if entry.collector != nil && entry.hint != nil {
+ if err := entry.collector.Add(entry.hint); err != nil {
+ return fmt.Errorf("failed to add hint to collector: %w", err)
+ }
+ }
+ }
+
+ // Phase 2: Update in-memory indexes (for current runtime correctness)
+ // This ensures the database continues to work correctly after merge
+ for _, entry := range job.lookup {
+ if entry == nil {
+ continue
+ }
+ job.applyLookup(entry)
+ }
+
+ // Update merge manifest with completion status
+ if len(job.outputs) == 0 {
+ job.manifest.MergeSeqMax = -1
+ } else {
+ job.manifest.MergeSeqMax = job.outputs[len(job.outputs)-1].seq
+ }
+ job.manifest.Status = manifestStatusCommitted
+ if err := writeMergeManifest(job.db.opt.Dir, job.manifest); err != nil {
+ return err
+ }
+
+ // Prepare list of old files for cleanup
+ job.oldData = job.oldData[:0]
+ job.oldHints = job.oldHints[:0]
+ for _, fid := range job.pending {
+ job.oldData = append(job.oldData, getDataPath(fid, job.db.opt.Dir))
+ job.oldHints = append(job.oldHints, getHintPath(fid, job.db.opt.Dir))
+ }
+
+ return nil
+}
+
+// cleanupOldFiles removes the old data and hint files that were merged, as well as the manifest file.
+// This is called after a successful merge to reclaim disk space.
+func (job *mergeV2Job) cleanupOldFiles() error {
+ // Close and remove old data files
+ for _, path := range job.oldData {
+ _ = job.db.fm.fdm.CloseByPath(path)
+ if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ }
+ // Remove old hint files
+ for _, path := range job.oldHints {
+ if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
+ }
+ // Remove the merge manifest file
+ return removeMergeManifest(job.db.opt.Dir)
+}
+
+// abort cleans up all created files when a merge fails and returns the original error.
+// It ensures no partial merge state is left behind and combines any cleanup errors.
+func (job *mergeV2Job) abort(err error) error {
+ var errs []error
+
+ // Clean up all output files created during the failed merge
+ for _, out := range job.outputs {
+ if out != nil {
+ if finalizeErr := out.finalize(); finalizeErr != nil {
+ errs = append(errs, fmt.Errorf("failed to finalize output %d: %w", out.seq, finalizeErr))
+ }
+ if out.dataPath != "" {
+ if removeErr := os.Remove(out.dataPath); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
+ errs = append(errs, fmt.Errorf("failed to remove data file %s: %w", out.dataPath, removeErr))
+ }
+ }
+ if out.hintPath != "" {
+ if removeErr := os.Remove(out.hintPath); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
+ errs = append(errs, fmt.Errorf("failed to remove hint file %s: %w", out.hintPath, removeErr))
+ }
+ }
+ }
+ }
+
+ // Clean up merge manifest file
+ if manifestErr := removeMergeManifest(job.db.opt.Dir); manifestErr != nil {
+ errs = append(errs, fmt.Errorf("failed to remove merge manifest: %w", manifestErr))
+ }
+
+ // If there are cleanup errors, add them to the original error
+ if len(errs) > 0 {
+ return fmt.Errorf("merge aborted with error: %w, cleanup errors: %v", err, errs)
+ }
+
+ return err
+}
+
+// rewriteFile processes a single data file during merge, rewriting valid entries to new merge files.
+// It reads entries sequentially, filters out invalid/expired entries, and rewrites remaining entries.
+func (job *mergeV2Job) rewriteFile(fid int64) error {
+ path := getDataPath(fid, job.db.opt.Dir)
+ fr, err := newFileRecovery(path, job.db.opt.BufferSizeOfRecovery)
+ if err != nil {
+ return fmt.Errorf("failed to create file recovery for %s: %w", path, err)
+ }
+ defer func() {
+ if releaseErr := fr.release(); releaseErr != nil {
+ // Log the error but don't override the original error
+ // In a production environment, you might want to log this
+ }
+ }()
+
+ off := int64(0)
+ for {
+ if off >= fr.size {
+ break
+ }
+ entry, err := fr.readEntry(off)
+ if err != nil {
+ if errors.Is(err, io.EOF) || errors.Is(err, ErrIndexOutOfBound) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, ErrHeaderSizeOutOfBounds) {
+ break
+ }
+ return fmt.Errorf("merge rewrite read entry at offset %d: %w", off, err)
+ }
+ if entry == nil {
+ break
+ }
+ sz := entry.Size()
+
+ // Validate entry size to prevent issues with invalid data
+ if sz <= 0 {
+ off++
+ continue
+ }
+
+ off += sz
+
+ // Skip entries that are not committed
+ if entry.Meta.Status != Committed {
+ continue
+ }
+ // Skip filter entries
+ if entry.isFilter() {
+ continue
+ }
+ // Skip expired entries
+ if data.IsExpired(entry.Meta.TTL, entry.Meta.Timestamp) {
+ continue
+ }
+
+ // Check if entry is still pending merge (not overwritten by newer version)
+ job.db.mu.RLock()
+ pending := job.db.isPendingMergeEntry(entry)
+ job.db.mu.RUnlock()
+ if !pending {
+ continue
+ }
+
+ // Allow custom processing of entries during rewrite
+ if job.onRewriteEntry != nil {
+ job.onRewriteEntry(entry)
+ }
+
+ if err := job.writeEntry(entry); err != nil {
+ return fmt.Errorf("failed to write entry: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// writeEntry writes an entry to the appropriate merge output file and creates the corresponding hint entry.
+// It also calculates value hashes for Set and SortedSet data structures to support duplicate detection.
+func (job *mergeV2Job) writeEntry(entry *Entry) error {
+ if entry == nil {
+ return fmt.Errorf("cannot write nil entry")
+ }
+
+ data := entry.Encode()
+ if len(data) == 0 {
+ return fmt.Errorf("encoded entry is empty")
+ }
+
+ // Get or create appropriate output file for this entry size
+ out, err := job.ensureOutput(int64(len(data)))
+ if err != nil {
+ return fmt.Errorf("failed to ensure output: %w", err)
+ }
+
+ if out == nil {
+ return fmt.Errorf("output is nil")
+ }
+
+ // Write the encoded entry data to the output file
+ offset := out.writeOff
+ if _, err := out.dataFile.WriteAt(data, offset); err != nil {
+ return fmt.Errorf("failed to write data at offset %d: %w", offset, err)
+ }
+ out.writeOff += int64(len(data))
+
+ // Create hint entry for fast index lookup
+ hint := newHintEntryFromEntry(entry, out.fileID, uint64(offset))
+
+ lookupEntry := &mergeLookupEntry{
+ hint: hint,
+ collector: out.collector,
+ }
+
+ // For Set and SortedSet, compute value hash to handle duplicate detection
+ if entry.Meta.Ds == DataStructureSet || entry.Meta.Ds == DataStructureSortedSet {
+ h := job.valueHasher
+ h.Reset()
+ if _, err := h.Write(entry.Value); err != nil {
+ return fmt.Errorf("failed to compute value hash: %w", err)
+ }
+ lookupEntry.valueHash = h.Sum32()
+ lookupEntry.hasValueHash = true
+ }
+
+ // Store lookup entry for later index update during commit phase
+ job.lookup = append(job.lookup, lookupEntry)
+
+ return nil
+}
+
+// ensureOutput returns the appropriate output file for writing entries of the given size.
+// If no output exists or the current output would exceed segment size, creates a new output.
+func (job *mergeV2Job) ensureOutput(size int64) (*mergeOutput, error) {
+ if size <= 0 {
+ return nil, fmt.Errorf("invalid size: %d", size)
+ }
+
+ // If no outputs exist yet, create the first one
+ if len(job.outputs) == 0 {
+ return job.newOutput()
+ }
+
+ // Get the current output file
+ cur := job.outputs[len(job.outputs)-1]
+ if cur == nil {
+ return job.newOutput()
+ }
+
+ // Check if adding this entry would exceed the segment size limit
+ if cur.writeOff+size > job.db.opt.SegmentSize {
+ return job.newOutput()
+ }
+
+ return cur, nil
+}
+
+// newOutput creates a new merge output file with associated hint file collector.
+// It generates unique file IDs using merge sequence numbers and cleans up any existing files.
+func (job *mergeV2Job) newOutput() (*mergeOutput, error) {
+ seq := job.outputSeqBase + len(job.outputs)
+ fileID := GetMergeFileID(seq)
+ dataPath := getMergeDataPath(job.db.opt.Dir, seq)
+ hintPath := getMergeHintPath(job.db.opt.Dir, seq)
+
+ // Clean up any existing files from previous failed merges
+ if err := os.Remove(dataPath); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return nil, fmt.Errorf("failed to remove old data file %s: %w", dataPath, err)
+ }
+ if err := os.Remove(hintPath); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return nil, fmt.Errorf("failed to remove old hint file %s: %w", hintPath, err)
+ }
+
+ // Create the data file with merge-specific file ID
+ dataFile, err := job.db.fm.GetDataFileByID(job.db.opt.Dir, fileID, job.db.opt.SegmentSize)
+ if err != nil {
+ return nil, fmt.Errorf("failed to get data file: %w", err)
+ }
+
+ // Create hint file writer and collector if hint files are enabled
+ var hintWriter *HintFileWriter
+ var collector *HintCollector
+ if job.db.opt.EnableHintFile {
+ hintWriter = &HintFileWriter{}
+ if err := hintWriter.Create(hintPath); err != nil {
+ // If hint writer creation fails, clean up the created data file
+ _ = dataFile.Close()
+ _ = os.Remove(dataPath)
+ return nil, fmt.Errorf("failed to create hint writer: %w", err)
+ }
+ collector = NewHintCollector(fileID, hintWriter, DefaultHintCollectorFlushEvery)
+ }
+
+ out := &mergeOutput{
+ seq: seq,
+ fileID: fileID,
+ dataFile: dataFile,
+ collector: collector,
+ dataPath: dataPath,
+ hintPath: hintPath,
+ }
+ job.outputs = append(job.outputs, out)
+ return out, nil
+}
+
+// finalize properly closes and syncs the output files, ensuring data is persisted to disk.
+// This method can be called multiple times safely (idempotent operation).
+func (out *mergeOutput) finalize() error {
+ if out.finalized {
+ return nil
+ }
+
+ var errs []error
+
+ // Close hint collector and flush any pending hints
+ if out.collector != nil {
+ if err := out.collector.Close(); err != nil && !errors.Is(err, errHintCollectorClosed) {
+ errs = append(errs, fmt.Errorf("failed to close hint collector: %w", err))
+ }
+ }
+
+ // Sync data file to ensure all writes are persisted to disk
+ if out.dataFile != nil {
+ if err := out.dataFile.Sync(); err != nil {
+ errs = append(errs, fmt.Errorf("failed to sync data file: %w", err))
+ }
+ // Close the data file
+ if err := out.dataFile.Close(); err != nil {
+ errs = append(errs, fmt.Errorf("failed to close data file: %w", err))
+ }
+ }
+
+ // Mark as finalized to prevent double-finalization
+ out.finalized = true
+
+ // Return any errors that occurred during finalization
+ if len(errs) > 0 {
+ return fmt.Errorf("finalize errors: %v", errs)
+ }
+
+ return nil
+}
+
+// updateRecordWithHintIfNewer updates a record with hint data only if the hint is newer or same timestamp.
+// This prevents overwriting newer entries that were written after merge started.
+func updateRecordWithHintIfNewer(record *data.Record, hint *HintEntry) bool {
+ // Only update if our hint is newer or same timestamp (don't overwrite newer data)
+ if record.Timestamp <= hint.Timestamp {
+ record.FileID = hint.FileID
+ record.DataPos = hint.DataPos
+ record.Timestamp = hint.Timestamp
+ record.TTL = hint.TTL
+ record.ValueSize = hint.ValueSize
+ return true
+ }
+ return false
+}
+
+// applyLookup updates in-memory indexes with the merged entry's new location.
+// This ensures runtime correctness even if concurrent updates happened during merge.
+// The function handles all supported data structures: BTree, Set, List, and SortedSet.
+//
+// IMPORTANT: We check timestamps to avoid overwriting newer entries that were written
+// after the merge started. The initial check happens in rewriteFile, but the index mutex
+// is released before commit, so newer entries might exist in the index now.
+func (job *mergeV2Job) applyLookup(entry *mergeLookupEntry) {
+ if entry == nil || entry.hint == nil {
+ return
+ }
+
+ hint := entry.hint
+ bucketID := BucketId(hint.BucketId)
+
+ switch hint.Ds {
+ case DataStructureBTree:
+ // Update BTree index with new file location if hint is newer or same age
+ bt, exist := job.db.Index.bTree.exist(bucketID)
+ if !exist {
+ return
+ }
+ record, ok := bt.Find(hint.Key)
+ if !ok || record == nil {
+ return
+ }
+ updateRecordWithHintIfNewer(record, hint)
+
+ case DataStructureSet:
+ // Update Set index using value hash for duplicate detection
+ setIdx, exist := job.db.Index.set.exist(bucketID)
+ if !exist {
+ return
+ }
+ members, ok := setIdx.M[string(hint.Key)]
+ if !ok {
+ return
+ }
+ // Value hash is required for Set to identify the specific member
+ if !entry.hasValueHash {
+ return
+ }
+ record, ok := members[entry.valueHash]
+ if !ok || record == nil {
+ return
+ }
+ updateRecordWithHintIfNewer(record, hint)
+
+ case DataStructureList:
+ // Update List index entries (only push operations are merged)
+ if hint.Flag != DataLPushFlag && hint.Flag != DataRPushFlag {
+ return
+ }
+ listIdx, exist := job.db.Index.list.exist(bucketID)
+ if !exist {
+ return
+ }
+ // Decode list key to extract user key and sequence number
+ userKey, seq := decodeListKey(hint.Key)
+ if userKey == nil {
+ return
+ }
+ items, ok := listIdx.Items[string(userKey)]
+ if !ok {
+ return
+ }
+ // Find the specific list item by sequence number
+ record, ok := items.Find(utils.ConvertUint64ToBigEndianBytes(seq))
+ if !ok || record == nil {
+ return
+ }
+ updateRecordWithHintIfNewer(record, hint)
+
+ case DataStructureSortedSet:
+ // Update SortedSet index using both key and value hash
+ sortedIdx, exist := job.db.Index.sortedSet.exist(bucketID)
+ if !exist {
+ return
+ }
+ // Extract member key from the encoded key
+ key, _ := splitStringFloat64Str(string(hint.Key), SeparatorForZSetKey)
+ if key == "" {
+ return
+ }
+ sl, ok := sortedIdx.M[key]
+ if !ok {
+ return
+ }
+ // Value hash is required for SortedSet to identify the specific member
+ if !entry.hasValueHash {
+ return
+ }
+ node, ok := sl.dict[entry.valueHash]
+ if !ok || node == nil {
+ return
+ }
+ record := node.record
+ if record == nil {
+ return
+ }
+ updateRecordWithHintIfNewer(record, hint)
+ }
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/metadata.go b/vendor/github.com/nutsdb/nutsdb/metadata.go
index 5c7f82b80d..d8d74c98cb 100644
--- a/vendor/github.com/nutsdb/nutsdb/metadata.go
+++ b/vendor/github.com/nutsdb/nutsdb/metadata.go
@@ -1,5 +1,10 @@
package nutsdb
+import (
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/utils"
+)
+
// DataStructure represents the data structure we have already supported
type DataStructure = uint16
@@ -80,6 +85,10 @@ const (
// DataExpireListFlag represents that set ttl for the list
DataExpireListFlag DataFlag = 19
+
+ // DataBucketDeleteFlag represents the delete bucket flag
+ // This is used for watch manager to delete the bucket when the bucket is deleted.
+ DataBucketDeleteFlag DataFlag = 20
)
const (
@@ -91,7 +100,7 @@ const (
)
// Persistent represents the data persistent flag
-const Persistent uint32 = 0
+const Persistent uint32 = data.Persistent
type MetaData struct {
KeySize uint32
@@ -111,15 +120,15 @@ func (meta *MetaData) Size() int64 {
// CRC
size := 4
- size += UvarintSize(uint64(meta.KeySize))
- size += UvarintSize(uint64(meta.ValueSize))
- size += UvarintSize(meta.Timestamp)
- size += UvarintSize(uint64(meta.TTL))
- size += UvarintSize(uint64(meta.Flag))
- size += UvarintSize(meta.TxID)
- size += UvarintSize(uint64(meta.Status))
- size += UvarintSize(uint64(meta.Ds))
- size += UvarintSize(meta.BucketId)
+ size += utils.UvarintSize(uint64(meta.KeySize))
+ size += utils.UvarintSize(uint64(meta.ValueSize))
+ size += utils.UvarintSize(meta.Timestamp)
+ size += utils.UvarintSize(uint64(meta.TTL))
+ size += utils.UvarintSize(uint64(meta.Flag))
+ size += utils.UvarintSize(meta.TxID)
+ size += utils.UvarintSize(uint64(meta.Status))
+ size += utils.UvarintSize(uint64(meta.Ds))
+ size += utils.UvarintSize(meta.BucketId)
return int64(size)
}
@@ -187,7 +196,7 @@ func (meta *MetaData) WithBucketId(bucketID uint64) *MetaData {
return meta
}
-func (meta *MetaData) IsBPlusTree() bool {
+func (meta *MetaData) IsBTree() bool {
return meta.Ds == DataStructureBTree
}
diff --git a/vendor/github.com/nutsdb/nutsdb/options.go b/vendor/github.com/nutsdb/nutsdb/options.go
index a1ae93107a..325a326281 100644
--- a/vendor/github.com/nutsdb/nutsdb/options.go
+++ b/vendor/github.com/nutsdb/nutsdb/options.go
@@ -14,7 +14,12 @@
package nutsdb
-import "time"
+import (
+ "time"
+
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/fileio"
+)
// EntryIdxMode represents entry index mode.
type EntryIdxMode int
@@ -37,6 +42,25 @@ const (
TimeHeap
)
+// ListImplementationType defines the implementation type for List data structure.
+type ListImplementationType data.ListImplementationType
+
+func (impl ListImplementationType) toInternal() data.ListImplementationType {
+ return data.ListImplementationType(impl)
+}
+
+const (
+ // ListImplDoublyLinkedList uses doubly linked list implementation (default).
+ // Advantages: O(1) head/tail operations, lower memory overhead
+ // Best for: High-frequency LPush/RPush/LPop/RPop operations
+ ListImplDoublyLinkedList = iota
+
+ // ListImplBTree uses BTree implementation.
+ // Advantages: O(log n + k) range queries, efficient random access
+ // Best for: Frequent range queries or indexed access patterns
+ ListImplBTree
+)
+
// An ErrorHandler handles an error occurred during transaction.
type ErrorHandler interface {
HandleError(err error)
@@ -84,7 +108,7 @@ type Options struct {
// BufferSizeOfRecovery represents the buffer size of recoveryReader buffer Size
BufferSizeOfRecovery int
- // CcWhenClose represent initiative GC when calling db.Close()
+ // GcWhenClose represent initiative GC when calling db.Close()
GCWhenClose bool
// CommitBufferSize represent allocated memory for tx
@@ -121,16 +145,30 @@ type Options struct {
// cache size for HintKeyAndRAMIdxMode
HintKeyAndRAMIdxCacheSize int
-}
-const (
- B = 1
+ // EnableHintFile represents if enable hint file feature.
+ // If EnableHintFile is true, hint files will be created and used for faster database startup.
+ // If EnableHintFile is false, hint files will not be created or used.
+ EnableHintFile bool
- KB = 1024 * B
+ // EnableMergeV2 toggles the redesigned merge pipeline with deterministic merge files and manifest support.
+ // When disabled, NutsDB falls back to the legacy merge logic.
+ EnableMergeV2 bool
- MB = 1024 * KB
+ // ListImpl specifies the implementation type for List data structure.
+ // Default: ListImplDoublyLinkedList (maintains backward compatibility)
+ ListImpl ListImplementationType
+
+ // EnableWatch toggles the watch feature.
+ // If EnableWatch is true, the watch feature will be enabled. The watch feature will be disabled by default.
+ EnableWatch bool
+}
- GB = 1024 * MB
+const (
+ B = fileio.B
+ KB = fileio.KB
+ MB = fileio.MB
+ GB = fileio.GB
)
// defaultSegmentSize is default data file size.
@@ -139,17 +177,40 @@ var defaultSegmentSize int64 = 256 * MB
// DefaultOptions represents the default options.
var DefaultOptions = func() Options {
return Options{
- EntryIdxMode: HintKeyValAndRAMIdxMode,
- SegmentSize: defaultSegmentSize,
- NodeNum: 1,
- RWMode: FileIO,
- SyncEnable: true,
- CommitBufferSize: 4 * MB,
- MergeInterval: 2 * time.Hour,
- MaxBatchSize: (15 * defaultSegmentSize / 4) / 100,
- MaxBatchCount: (15 * defaultSegmentSize / 4) / 100 / 100,
+ EntryIdxMode: HintKeyValAndRAMIdxMode,
+ SegmentSize: defaultSegmentSize,
+ NodeNum: 1,
+ RWMode: FileIO,
+ SyncEnable: true,
+ CommitBufferSize: 4 * MB,
+ MergeInterval: 2 * time.Hour,
+ MaxBatchSize: (15 * defaultSegmentSize / 4) / 100,
+ MaxBatchCount: (15 * defaultSegmentSize / 4) / 100 / 100,
+ HintKeyAndRAMIdxCacheSize: 0,
+ ExpiredDeleteType: TimeWheel,
+ EnableHintFile: false,
+ EnableMergeV2: false,
+ ListImpl: ListImplementationType(ListImplBTree),
+ EnableWatch: false,
+ }
+}()
+
+var doublyLinkedListOptions = func() Options {
+ return Options{
+ EntryIdxMode: HintKeyValAndRAMIdxMode,
+ SegmentSize: defaultSegmentSize,
+ NodeNum: 1,
+ RWMode: FileIO,
+ SyncEnable: true,
+ CommitBufferSize: 4 * MB,
+ MergeInterval: 2 * time.Hour,
+ MaxBatchSize: (15 * defaultSegmentSize / 4) / 100,
+ MaxBatchCount: (15 * defaultSegmentSize / 4) / 100 / 100,
HintKeyAndRAMIdxCacheSize: 0,
- ExpiredDeleteType: TimeWheel,
+ ExpiredDeleteType: TimeWheel,
+ EnableHintFile: false,
+ EnableMergeV2: false,
+ ListImpl: ListImplementationType(ListImplDoublyLinkedList),
}
}()
@@ -186,9 +247,9 @@ func WithMaxBatchCount(count int64) Option {
}
func WithHintKeyAndRAMIdxCacheSize(size int) Option {
- return func(opt *Options) {
- opt.HintKeyAndRAMIdxCacheSize = size
- }
+ return func(opt *Options) {
+ opt.HintKeyAndRAMIdxCacheSize = size
+ }
}
func WithMaxBatchSize(size int64) Option {
@@ -256,3 +317,21 @@ func WithMaxWriteRecordCount(maxWriteRecordCount int64) Option {
opt.MaxWriteRecordCount = maxWriteRecordCount
}
}
+
+func WithEnableHintFile(enable bool) Option {
+ return func(opt *Options) {
+ opt.EnableHintFile = enable
+ }
+}
+
+func WithEnableMergeV2(enable bool) Option {
+ return func(opt *Options) {
+ opt.EnableMergeV2 = enable
+ }
+}
+
+func WithListImpl(implType ListImplementationType) Option {
+ return func(opt *Options) {
+ opt.ListImpl = implType
+ }
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/pending.go b/vendor/github.com/nutsdb/nutsdb/pending.go
index a98697fda5..09f63d01f1 100644
--- a/vendor/github.com/nutsdb/nutsdb/pending.go
+++ b/vendor/github.com/nutsdb/nutsdb/pending.go
@@ -1,5 +1,10 @@
package nutsdb
+import (
+ "bytes"
+ "sort"
+)
+
// EntryStatus represents the Entry status in the current Tx
type EntryStatus = uint8
@@ -59,9 +64,9 @@ func (pending *pendingEntryList) submitEntry(ds Ds, bucket string, e *Entry) {
pending.entriesInBTree[bucket] = map[string]*Entry{}
}
if _, exist := pending.entriesInBTree[bucket][string(e.Key)]; !exist {
- pending.entriesInBTree[bucket][string(e.Key)] = e
pending.size++
}
+ pending.entriesInBTree[bucket][string(e.Key)] = e
default:
if _, exist := pending.entries[ds]; !exist {
pending.entries[ds] = map[BucketName][]*Entry{}
@@ -73,6 +78,68 @@ func (pending *pendingEntryList) submitEntry(ds Ds, bucket string, e *Entry) {
}
}
+func (pending *pendingEntryList) Get(ds Ds, bucket string, key []byte) (entry *Entry, err error) {
+ switch ds {
+ case DataStructureBTree:
+ if _, exist := pending.entriesInBTree[bucket]; exist {
+ if rec, ok := pending.entriesInBTree[bucket][string(key)]; ok {
+ return rec, nil
+ } else {
+ return nil, ErrKeyNotFound
+ }
+ }
+ return nil, ErrBucketNotFound
+ default:
+ if _, exist := pending.entries[ds]; exist {
+ if entries, ok := pending.entries[ds][bucket]; ok {
+ for _, e := range entries {
+ if bytes.Equal(key, e.Key) {
+ return e, nil
+ }
+ }
+ return nil, ErrKeyNotFound
+ } else {
+ return nil, ErrKeyNotFound
+ }
+ }
+ return nil, ErrBucketNotFound
+ }
+}
+
+func (pending *pendingEntryList) GetTTL(ds Ds, bucket string, key []byte) (ttl int64, err error) {
+ rec, err := pending.Get(ds, bucket, key)
+ if err != nil {
+ return 0, err
+ }
+ if rec.Meta.TTL == Persistent {
+ return -1, nil
+ }
+ return int64(expireTime(rec.Meta.Timestamp, rec.Meta.TTL).Seconds()), nil
+}
+
+func (pending *pendingEntryList) getDataByRange(
+ start, end []byte, bucketName BucketName,
+) (keys, values [][]byte) {
+
+ mp, ok := pending.entriesInBTree[bucketName]
+ if !ok {
+ return nil, nil
+ }
+ keys = make([][]byte, 0)
+ values = make([][]byte, 0)
+ for _, v := range mp {
+ if bytes.Compare(start, v.Key) <= 0 && bytes.Compare(v.Key, end) <= 0 {
+ keys = append(keys, v.Key)
+ values = append(values, v.Value)
+ }
+ }
+ sort.Sort(&sortkv{
+ k: keys,
+ v: values,
+ })
+ return
+}
+
// rangeBucket input a range handler function f and call it with every bucket in pendingBucketList
func (p pendingBucketList) rangeBucket(f func(bucket *Bucket) error) error {
for _, bucketsInDs := range p {
@@ -96,10 +163,54 @@ func (pending *pendingEntryList) toList() []*Entry {
}
for _, entriesInDS := range pending.entries {
for _, entries := range entriesInDS {
- for _, entry := range entries {
- list = append(list, entry)
- }
+ list = append(list, entries...)
}
}
return list
}
+
+func (pending *pendingEntryList) rangeEntries(_ Ds, bucketName BucketName, rangeFunc func(entry *Entry) bool) {
+ pendingWriteEntries := pending.entriesInBTree
+ if pendingWriteEntries == nil {
+ return
+ }
+ entries := pendingWriteEntries[bucketName]
+ for _, entry := range entries {
+ ok := rangeFunc(entry)
+ if !ok {
+ break
+ }
+ }
+}
+
+func (pending *pendingEntryList) MaxOrMinKey(bucketName string, isMax bool) (key []byte, found bool) {
+ var (
+ maxKey []byte = nil
+ minKey []byte = nil
+ pendingFound = false
+ )
+
+ pending.rangeEntries(
+ DataStructureBTree,
+ bucketName,
+ func(entry *Entry) bool {
+ maxKey = compareAndReturn(maxKey, entry.Key, 1)
+ minKey = compareAndReturn(minKey, entry.Key, -1)
+ pendingFound = true
+ return true
+ })
+
+ if !pendingFound {
+ return nil, false
+ }
+ if isMax {
+ return maxKey, true
+ }
+ return minKey, true
+}
+
+// isBucketNotFoundStatus return true for bucket is not found,
+// false for other status.
+func isBucketNotFoundStatus(status BucketStatus) bool {
+ return status == BucketStatusDeleted || status == BucketStatusUnknown
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/rwmanger_mmap.go b/vendor/github.com/nutsdb/nutsdb/rwmanger_mmap.go
deleted file mode 100644
index 5f6154368e..0000000000
--- a/vendor/github.com/nutsdb/nutsdb/rwmanger_mmap.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright 2019 The nutsdb Author. All rights reserved.
-//
-// Licensed 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 nutsdb
-
-import (
- "errors"
-
- mmap "github.com/xujiajun/mmap-go"
-)
-
-// MMapRWManager represents the RWManager which using mmap.
-type MMapRWManager struct {
- path string
- fdm *fdManager
- m mmap.MMap
- segmentSize int64
-}
-
-var (
- // ErrUnmappedMemory is returned when a function is called on unmapped memory
- ErrUnmappedMemory = errors.New("unmapped memory")
-
- // ErrIndexOutOfBound is returned when given offset out of mapped region
- ErrIndexOutOfBound = errors.New("offset out of mapped region")
-)
-
-// WriteAt copies data to mapped region from the b slice starting at
-// given off and returns number of bytes copied to the mapped region.
-func (mm *MMapRWManager) WriteAt(b []byte, off int64) (n int, err error) {
- if mm.m == nil {
- return 0, ErrUnmappedMemory
- } else if off >= int64(len(mm.m)) || off < 0 {
- return 0, ErrIndexOutOfBound
- }
-
- return copy(mm.m[off:], b), nil
-}
-
-// ReadAt copies data to b slice from mapped region starting at
-// given off and returns number of bytes copied to the b slice.
-func (mm *MMapRWManager) ReadAt(b []byte, off int64) (n int, err error) {
- if mm.m == nil {
- return 0, ErrUnmappedMemory
- } else if off >= int64(len(mm.m)) || off < 0 {
- return 0, ErrIndexOutOfBound
- }
-
- return copy(b, mm.m[off:]), nil
-}
-
-// Sync synchronizes the mapping's contents to the file's contents on disk.
-func (mm *MMapRWManager) Sync() (err error) {
- return mm.m.Flush()
-}
-
-// Release deletes the memory mapped region, flushes any remaining changes
-func (mm *MMapRWManager) Release() (err error) {
- mm.fdm.reduceUsing(mm.path)
- return mm.m.Unmap()
-}
-
-func (mm *MMapRWManager) Size() int64 {
- return mm.segmentSize
-}
-
-// Close will remove the cache in the fdm of the specified path, and call the close method of the os of the file
-func (mm *MMapRWManager) Close() (err error) {
- return mm.fdm.closeByPath(mm.path)
-}
diff --git a/vendor/github.com/nutsdb/nutsdb/sorted_set.go b/vendor/github.com/nutsdb/nutsdb/sorted_set.go
index ee2d0c232a..1873d3ec15 100644
--- a/vendor/github.com/nutsdb/nutsdb/sorted_set.go
+++ b/vendor/github.com/nutsdb/nutsdb/sorted_set.go
@@ -18,6 +18,9 @@ import (
"bytes"
"errors"
"math/rand"
+
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/utils"
)
var (
@@ -48,7 +51,7 @@ func NewSortedSet(db *DB) *SortedSet {
}
}
-func (z *SortedSet) ZAdd(key string, score SCORE, value []byte, record *Record) error {
+func (z *SortedSet) ZAdd(key string, score SCORE, value []byte, record *data.Record) error {
sortedSet, ok := z.M[key]
if !ok {
z.M[key] = newSkipList(z.db)
@@ -58,7 +61,7 @@ func (z *SortedSet) ZAdd(key string, score SCORE, value []byte, record *Record)
return sortedSet.Put(score, value, record)
}
-func (z *SortedSet) ZMembers(key string) (map[*Record]SCORE, error) {
+func (z *SortedSet) ZMembers(key string) (map[*data.Record]SCORE, error) {
sortedSet, ok := z.M[key]
if !ok {
@@ -67,7 +70,7 @@ func (z *SortedSet) ZMembers(key string) (map[*Record]SCORE, error) {
nodes := sortedSet.dict
- members := make(map[*Record]SCORE, len(nodes))
+ members := make(map[*data.Record]SCORE, len(nodes))
for _, node := range nodes {
members[node.record] = node.score
}
@@ -90,7 +93,7 @@ func (z *SortedSet) ZCount(key string, start SCORE, end SCORE, opts *GetByScoreR
return 0, ErrSortedSetNotFound
}
-func (z *SortedSet) ZPeekMax(key string) (*Record, SCORE, error) {
+func (z *SortedSet) ZPeekMax(key string) (*data.Record, SCORE, error) {
if sortedSet, ok := z.M[key]; ok {
node := sortedSet.PeekMax()
if node != nil {
@@ -102,7 +105,7 @@ func (z *SortedSet) ZPeekMax(key string) (*Record, SCORE, error) {
return nil, 0, ErrSortedSetNotFound
}
-func (z *SortedSet) ZPopMax(key string) (*Record, SCORE, error) {
+func (z *SortedSet) ZPopMax(key string) (*data.Record, SCORE, error) {
if sortedSet, ok := z.M[key]; ok {
node := sortedSet.PopMax()
if node != nil {
@@ -114,7 +117,7 @@ func (z *SortedSet) ZPopMax(key string) (*Record, SCORE, error) {
return nil, 0, ErrSortedSetNotFound
}
-func (z *SortedSet) ZPeekMin(key string) (*Record, SCORE, error) {
+func (z *SortedSet) ZPeekMin(key string) (*data.Record, SCORE, error) {
if sortedSet, ok := z.M[key]; ok {
node := sortedSet.PeekMin()
if node != nil {
@@ -126,7 +129,7 @@ func (z *SortedSet) ZPeekMin(key string) (*Record, SCORE, error) {
return nil, 0, ErrSortedSetNotFound
}
-func (z *SortedSet) ZPopMin(key string) (*Record, SCORE, error) {
+func (z *SortedSet) ZPopMin(key string) (*data.Record, SCORE, error) {
if sortedSet, ok := z.M[key]; ok {
node := sortedSet.PopMin()
if node != nil {
@@ -138,12 +141,12 @@ func (z *SortedSet) ZPopMin(key string) (*Record, SCORE, error) {
return nil, 0, ErrSortedSetNotFound
}
-func (z *SortedSet) ZRangeByScore(key string, start SCORE, end SCORE, opts *GetByScoreRangeOptions) ([]*Record, []float64, error) {
+func (z *SortedSet) ZRangeByScore(key string, start SCORE, end SCORE, opts *GetByScoreRangeOptions) ([]*data.Record, []float64, error) {
if sortedSet, ok := z.M[key]; ok {
nodes := sortedSet.GetByScoreRange(start, end, opts)
- records := make([]*Record, len(nodes))
+ records := make([]*data.Record, len(nodes))
scores := make([]float64, len(nodes))
for i, node := range nodes {
@@ -157,12 +160,12 @@ func (z *SortedSet) ZRangeByScore(key string, start SCORE, end SCORE, opts *GetB
return nil, nil, ErrSortedSetNotFound
}
-func (z *SortedSet) ZRangeByRank(key string, start int, end int) ([]*Record, []float64, error) {
+func (z *SortedSet) ZRangeByRank(key string, start int, end int) ([]*data.Record, []float64, error) {
if sortedSet, ok := z.M[key]; ok {
nodes := sortedSet.GetByRankRange(start, end, false)
- records := make([]*Record, len(nodes))
+ records := make([]*data.Record, len(nodes))
scores := make([]float64, len(nodes))
for i, node := range nodes {
@@ -176,9 +179,9 @@ func (z *SortedSet) ZRangeByRank(key string, start int, end int) ([]*Record, []f
return nil, nil, ErrSortedSetNotFound
}
-func (z *SortedSet) ZRem(key string, value []byte) (*Record, error) {
+func (z *SortedSet) ZRem(key string, value []byte) (*data.Record, error) {
if sortedSet, ok := z.M[key]; ok {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return nil, err
}
@@ -212,7 +215,7 @@ func (z *SortedSet) getZRemRangeByRankNodes(key string, start int, end int) ([]*
func (z *SortedSet) ZRank(key string, value []byte) (int, error) {
if sortedSet, ok := z.M[key]; ok {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return 0, err
}
@@ -227,7 +230,7 @@ func (z *SortedSet) ZRank(key string, value []byte) (int, error) {
func (z *SortedSet) ZRevRank(key string, value []byte) (int, error) {
if sortedSet, ok := z.M[key]; ok {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return 0, err
}
@@ -253,7 +256,7 @@ func (z *SortedSet) ZScore(key string, value []byte) (float64, error) {
func (z *SortedSet) ZExist(key string, value []byte) (bool, error) {
if sortedSet, ok := z.M[key]; ok {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return false, err
}
@@ -284,9 +287,9 @@ type SkipList struct {
// SkipListNode represents a node in the SkipList.
type SkipListNode struct {
- hash uint32 // unique key of this node
- record *Record // associated data
- score SCORE // score to determine the order of this node in the set
+ hash uint32 // unique key of this node
+ record *data.Record // associated data
+ score SCORE // score to determine the order of this node in the set
backward *SkipListNode
level []SkipListLevel
}
@@ -302,7 +305,7 @@ func (sln *SkipListNode) Score() SCORE {
}
// createNode returns a newly initialized SkipListNode Object that implements the SkipListNode.
-func createNode(level int, score SCORE, hash uint32, record *Record) *SkipListNode {
+func createNode(level int, score SCORE, hash uint32, record *data.Record) *SkipListNode {
node := SkipListNode{
hash: hash,
record: record,
@@ -335,18 +338,18 @@ func newSkipList(db *DB) *SkipList {
level: 1,
dict: make(map[uint32]*SkipListNode),
}
- hash, _ := getFnv32([]byte(""))
+ hash, _ := utils.GetFnv32([]byte(""))
skipList.header = createNode(SkipListMaxLevel, 0, hash, nil)
return skipList
}
-func (sl *SkipList) cmp(r1 *Record, r2 *Record) int {
+func (sl *SkipList) cmp(r1 *data.Record, r2 *data.Record) int {
val1, _ := sl.db.getValueByRecord(r1)
val2, _ := sl.db.getValueByRecord(r2)
return bytes.Compare(val1, val2)
}
-func (sl *SkipList) insertNode(score SCORE, hash uint32, record *Record) *SkipListNode {
+func (sl *SkipList) insertNode(score SCORE, hash uint32, record *data.Record) *SkipListNode {
var update [SkipListMaxLevel]*SkipListNode
var rank [SkipListMaxLevel]int64
@@ -511,10 +514,10 @@ func (sl *SkipList) PopMax() *SkipListNode {
// Put puts an element into the sorted set with specific key / value / score.
//
// Time complexity of this method is : O(log(N)).
-func (sl *SkipList) Put(score SCORE, value []byte, record *Record) error {
+func (sl *SkipList) Put(score SCORE, value []byte, record *data.Record) error {
var newNode *SkipListNode
- hash, _ := getFnv32(value)
+ hash, _ := utils.GetFnv32(value)
if n, ok := sl.dict[hash]; ok {
// score does not change, only update value
@@ -766,7 +769,7 @@ func (sl *SkipList) GetByRank(rank int, remove bool) *SkipListNode {
//
// Time complexity : O(1).
func (sl *SkipList) GetByValue(value []byte) *SkipListNode {
- hash, _ := getFnv32(value)
+ hash, _ := utils.GetFnv32(value)
return sl.dict[hash]
}
diff --git a/vendor/github.com/nutsdb/nutsdb/tx.go b/vendor/github.com/nutsdb/nutsdb/tx.go
index f085b979db..d13c211dc4 100644
--- a/vendor/github.com/nutsdb/nutsdb/tx.go
+++ b/vendor/github.com/nutsdb/nutsdb/tx.go
@@ -18,10 +18,12 @@ import (
"bytes"
"errors"
"fmt"
+ "log"
"strings"
"sync/atomic"
- "github.com/bwmarrin/snowflake"
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/utils"
"github.com/xujiajun/utils/strconv2"
)
@@ -96,8 +98,6 @@ func (db *DB) Begin(writable bool) (tx *Tx, err error) {
// newTx returns a newly initialized Tx object at given writable.
func newTx(db *DB, writable bool) (tx *Tx, err error) {
- var txID uint64
-
tx = &Tx{
db: db,
writable: writable,
@@ -105,12 +105,7 @@ func newTx(db *DB, writable bool) (tx *Tx, err error) {
pendingBucketList: make(map[Ds]map[BucketName]*Bucket),
}
- txID, err = tx.getTxID()
- if err != nil {
- return nil, err
- }
-
- tx.id = txID
+ tx.id = tx.getTxID()
return
}
@@ -160,15 +155,10 @@ func (tx *Tx) checkSize() error {
}
// getTxID returns the tx id.
-func (tx *Tx) getTxID() (id uint64, err error) {
- node, err := snowflake.NewNode(tx.db.opt.NodeNum)
- if err != nil {
- return 0, err
- }
-
- id = uint64(node.Generate().Int64())
-
- return
+// Uses cached snowflake node to avoid recreating for every transaction.
+func (tx *Tx) getTxID() uint64 {
+ node := tx.db.getSnowflakeNode()
+ return uint64(node.Generate().Int64())
}
// Commit commits the transaction, following these steps:
@@ -181,17 +171,21 @@ func (tx *Tx) getTxID() (id uint64, err error) {
//
// 4. build Hint index.
//
-// 5. Unlock the database and clear the db field.
+// 5. send updated entries to watch manager if watch feature is enabled.
+//
+// 6. Unlock the database and clear the db field.
func (tx *Tx) Commit() (err error) {
defer func() {
if err != nil {
tx.handleErr(err)
}
+
tx.unlock()
tx.db = nil
tx.pendingWrites = nil
}()
+
if tx.isClosed() {
return ErrCannotCommitAClosedTx
}
@@ -225,7 +219,7 @@ func (tx *Tx) Commit() (err error) {
buff := tx.allocCommitBuffer()
defer tx.db.commitBuffer.Reset()
- var records []*Record
+ var records []*data.Record
pendingWriteList := tx.pendingWrites.toList()
lastIndex := len(pendingWriteList) - 1
@@ -286,27 +280,21 @@ func (tx *Tx) Commit() (err error) {
return err
}
+ // send updated entries to watch manager
+ if tx.db.wm != nil {
+ tx.sendUpdatedEntries(pendingWriteList, tx.getDeletedBuckets())
+ }
+
return nil
}
func (tx *Tx) getNewAddRecordCount() (int64, error) {
var res int64
- changeCountInEntries := tx.getChangeCountInEntriesChanges()
+ changeCountInEntries, err := tx.getChangeCountInEntriesChanges()
changeCountInBucket := tx.getChangeCountInBucketChanges()
res += changeCountInEntries
res += changeCountInBucket
- return res, nil
-}
-
-func (tx *Tx) getListHeadTailSeq(bucketId BucketId, key string) *HeadTailSeq {
- res := HeadTailSeq{Head: initialListSeq, Tail: initialListSeq + 1}
- if _, ok := tx.db.Index.list.idx[bucketId]; ok {
- if _, ok := tx.db.Index.list.idx[bucketId].Seq[key]; ok {
- res = *tx.db.Index.list.idx[bucketId].Seq[key]
- }
- }
-
- return &res
+ return res, err
}
func (tx *Tx) getListEntryNewAddRecordCount(bucketId BucketId, entry *Entry) (int64, error) {
@@ -325,11 +313,11 @@ func (tx *Tx) getListEntryNewAddRecordCount(bucketId BucketId, entry *Entry) (in
case DataLPopFlag, DataRPopFlag:
res--
case DataLRemByIndex:
- indexes, _ := UnmarshalInts([]byte(value))
- res -= int64(len(l.getValidIndexes(key, indexes)))
+ indexes, _ := utils.UnmarshalInts([]byte(value))
+ res -= int64(len(l.GetValidIndexes(key, indexes)))
case DataLRemFlag:
count, newValue := splitIntStringStr(value, SeparatorForListKey)
- removeIndices, err := l.getRemoveIndexes(key, count, func(r *Record) (bool, error) {
+ removeIndices, err := l.GetRemoveIndexes(key, count, func(r *data.Record) (bool, error) {
v, err := tx.db.getValueByRecord(r)
if err != nil {
return false, err
@@ -384,7 +372,7 @@ func (tx *Tx) getKvEntryNewAddRecordCount(bucketId BucketId, entry *Entry) (int6
return res, nil
}
-func (tx *Tx) getSetEntryNewAddRecordCount(bucketId BucketId, entry *Entry) (int64, error) {
+func (tx *Tx) getSetEntryNewAddRecordCount(_ BucketId, entry *Entry) (int64, error) {
var res int64
if entry.Meta.Flag == DataDeleteFlag {
@@ -496,7 +484,7 @@ func (tx *Tx) rotateActiveFile() error {
// reset ActiveFile
path := getDataPath(tx.db.MaxFileID, tx.db.opt.Dir)
- tx.db.ActiveFile, err = tx.db.fm.getDataFile(path, tx.db.opt.SegmentSize)
+ tx.db.ActiveFile, err = tx.db.fm.GetDataFile(path, tx.db.opt.SegmentSize)
if err != nil {
return err
}
@@ -589,28 +577,20 @@ func (tx *Tx) checkTxIsClosed() error {
// put sets the value for a key in the bucket.
// Returns an error if tx is closed, if performing a write operation on a read-only transaction, if the key is empty.
-func (tx *Tx) put(bucket string, key, value []byte, ttl uint32, flag uint16, timestamp uint64, ds uint16) error {
+func (tx *Tx) put(bucket string, key, value []byte, ttl uint32, flag uint16, timestamp uint64, ds uint16) (err error) {
if err := tx.checkTxIsClosed(); err != nil {
return err
}
- bucketStatus := tx.getBucketStatus(DataStructureBTree, bucket)
- if bucketStatus == BucketStatusDeleted {
+ bucketStatus, b := tx.getBucketAndItsStatus(ds, bucket)
+ if isBucketNotFoundStatus(bucketStatus) {
return ErrBucketNotFound
}
- if !tx.db.bm.ExistBucket(ds, bucket) {
- return ErrorBucketNotExist
- }
-
if !tx.writable {
return ErrTxNotWritable
}
-
- bucketId, err := tx.db.bm.GetBucketID(ds, bucket)
- if err != nil {
- return err
- }
+ bucketId := b.Id
meta := NewMetaData().WithTimeStamp(timestamp).WithKeySize(uint32(len(key))).WithValueSize(uint32(len(value))).WithFlag(flag).
WithTTL(ttl).WithStatus(UnCommitted).WithDs(ds).WithTxID(tx.id).WithBucketId(bucketId)
@@ -622,11 +602,7 @@ func (tx *Tx) put(bucket string, key, value []byte, ttl uint32, flag uint16, tim
return err
}
tx.submitEntry(ds, bucket, e)
- if err != nil {
- return err
- }
tx.size += e.Size()
-
return nil
}
@@ -679,7 +655,7 @@ func (tx *Tx) isClosed() bool {
return status == txStatusClosed
}
-func (tx *Tx) buildIdxes(records []*Record, entries []*Entry) error {
+func (tx *Tx) buildIdxes(records []*data.Record, entries []*Entry) error {
for i, entry := range entries {
meta := entry.Meta
var err error
@@ -731,7 +707,8 @@ func (tx *Tx) SubmitBucket() error {
func (tx *Tx) buildBucketInIndex() error {
for _, mapper := range tx.pendingBucketList {
for _, bucket := range mapper {
- if bucket.Meta.Op == BucketInsertOperation {
+ switch bucket.Meta.Op {
+ case BucketInsertOperation:
switch bucket.Ds {
case DataStructureBTree:
tx.db.Index.bTree.getWithDefault(bucket.Id)
@@ -744,7 +721,7 @@ func (tx *Tx) buildBucketInIndex() error {
default:
return ErrDataStructureNotSupported
}
- } else if bucket.Meta.Op == BucketDeleteOperation {
+ case BucketDeleteOperation:
switch bucket.Ds {
case DataStructureBTree:
tx.db.Index.bTree.delete(bucket.Id)
@@ -763,14 +740,13 @@ func (tx *Tx) buildBucketInIndex() error {
return nil
}
-func (tx *Tx) getChangeCountInEntriesChanges() int64 {
+func (tx *Tx) getChangeCountInEntriesChanges() (int64, error) {
var res int64
- var err error
for _, entriesInDS := range tx.pendingWrites.entriesInBTree {
for _, entry := range entriesInDS {
- curRecordCnt, _ := tx.getEntryNewAddRecordCount(entry)
+ curRecordCnt, err := tx.getEntryNewAddRecordCount(entry)
if err != nil {
- return res
+ return res, nil
}
res += curRecordCnt
}
@@ -778,20 +754,20 @@ func (tx *Tx) getChangeCountInEntriesChanges() int64 {
for _, entriesInDS := range tx.pendingWrites.entries {
for _, entries := range entriesInDS {
for _, entry := range entries {
- curRecordCnt, _ := tx.getEntryNewAddRecordCount(entry)
+ curRecordCnt, err := tx.getEntryNewAddRecordCount(entry)
if err != nil {
- return res
+ return res, err
}
res += curRecordCnt
}
}
}
- return res
+ return res, nil
}
func (tx *Tx) getChangeCountInBucketChanges() int64 {
var res int64
- var f = func(bucket *Bucket) error {
+ f := func(bucket *Bucket) error {
bucketId := bucket.Id
if bucket.Meta.Op == BucketDeleteOperation {
switch bucket.Ds {
@@ -829,25 +805,27 @@ func (tx *Tx) getChangeCountInBucketChanges() int64 {
return res
}
-func (tx *Tx) getBucketStatus(ds Ds, name BucketName) BucketStatus {
+// getBucketAndItsStatus, get bucket and it is status in pendingBucketList,
+// if bucket is already in bucket manager but not in pendingList, will return BucketStatusExistAlready.
+func (tx *Tx) getBucketAndItsStatus(ds Ds, name BucketName) (BucketStatus, *Bucket) {
if len(tx.pendingBucketList) > 0 {
if bucketInDs, exist := tx.pendingBucketList[ds]; exist {
if bucket, exist := bucketInDs[name]; exist {
switch bucket.Meta.Op {
case BucketInsertOperation:
- return BucketStatusNew
+ return BucketStatusNew, bucket
case BucketDeleteOperation:
- return BucketStatusDeleted
+ return BucketStatusDeleted, bucket
case BucketUpdateOperation:
- return BucketStatusUpdated
+ return BucketStatusUpdated, bucket
}
}
}
}
- if tx.db.bm.ExistBucket(ds, name) {
- return BucketStatusExistAlready
+ if bucket, err := tx.db.bm.GetBucket(ds, name); err == nil {
+ return BucketStatusExistAlready, bucket
}
- return BucketStatusUnknown
+ return BucketStatusUnknown, nil
}
// findEntryStatus finds the latest status for the certain Entry in Tx
@@ -873,3 +851,52 @@ func (tx *Tx) findEntryAndItsStatus(ds Ds, bucket BucketName, key string) (Entry
}
return NotFoundEntry, nil
}
+
+/*
+ * send updated entries to watch manager for monitoring
+ * and specifying the buckets to be deleted
+ * @param pendingWriteList: the list of entries to be sent
+ * @param deletedBuckets: the buckets to be deleted
+ *
+ *
+ * @return: nil if success, error if any
+ */
+func (tx *Tx) sendUpdatedEntries(pendingWriteList []*Entry, deletedBuckets map[BucketName]bool) {
+ err := tx.db.wm.sendUpdatedEntries(pendingWriteList, deletedBuckets, func(bucketId BucketId) (BucketName, error) {
+ bucket, err := tx.db.bm.GetBucketById(bucketId)
+ if err != nil {
+ return "", err
+ }
+
+ return bucket.Name, nil
+ })
+
+ if err != nil {
+ log.Println("send updated entries error: ", err)
+ }
+}
+
+/*
+* send buckets to watch manager for specifying the bucket to be deleted
+
+* @param pendingWriteList: the list of entries to be sent
+* @return: nil if success, error if any
+ */
+func (tx *Tx) getDeletedBuckets() (deletedBuckets map[BucketName]bool) {
+ if len(tx.pendingBucketList) == 0 {
+ return nil
+ }
+
+ deletedBuckets = make(map[BucketName]bool)
+ for _, mapper := range tx.pendingBucketList {
+ for name, bucket := range mapper {
+ isAllDsDeleted := len(tx.db.bm.BucketIDMarker[name]) == 0
+ if _, ok := deletedBuckets[name]; !ok && bucket.Meta.Op == BucketDeleteOperation && isAllDsDeleted {
+ deletedBuckets[name] = true
+ }
+
+ }
+ }
+
+ return deletedBuckets
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/tx_btree.go b/vendor/github.com/nutsdb/nutsdb/tx_btree.go
index 46c2c81ffb..c44e5ce3b0 100644
--- a/vendor/github.com/nutsdb/nutsdb/tx_btree.go
+++ b/vendor/github.com/nutsdb/nutsdb/tx_btree.go
@@ -22,6 +22,7 @@ import (
"sync/atomic"
"time"
+ "github.com/nutsdb/nutsdb/internal/data"
"github.com/xujiajun/utils/strconv2"
)
@@ -47,15 +48,20 @@ func (tx *Tx) PutIfNotExists(bucket string, key, value []byte, ttl uint32) error
return err
}
- b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
- if err != nil {
- return err
+ status, b := tx.getBucketAndItsStatus(DataStructureBTree, bucket)
+ if isBucketNotFoundStatus(status) {
+ return ErrNotFoundBucket
}
bucketId := b.Id
+ _, err := tx.pendingWrites.Get(DataStructureBTree, bucket, key)
+ if err == nil {
+ // the key-value is exists.
+ return nil
+ }
idx, bucketExists := tx.db.Index.bTree.exist(bucketId)
if !bucketExists {
- return ErrNotFoundBucket
+ return tx.put(bucket, key, value, ttl, DataSetFlag, uint64(time.Now().UnixMilli()), DataStructureBTree)
}
record, recordExists := idx.Find(key)
@@ -66,7 +72,7 @@ func (tx *Tx) PutIfNotExists(bucket string, key, value []byte, ttl uint32) error
return tx.put(bucket, key, value, ttl, DataSetFlag, uint64(time.Now().UnixMilli()), DataStructureBTree)
}
-// PutIfExits set the value for a key in the bucket only if the key already exits.
+// PutIfExists set the value for a key in the bucket only if the key already exits.
func (tx *Tx) PutIfExists(bucket string, key, value []byte, ttl uint32) error {
return tx.update(bucket, key, func(_ []byte) ([]byte, error) {
return value, nil
@@ -86,24 +92,18 @@ func (tx *Tx) get(bucket string, key []byte) (value []byte, err error) {
return nil, err
}
- b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
- if err != nil {
- return nil, err
+ status, b := tx.getBucketAndItsStatus(DataStructureBTree, bucket)
+ if isBucketNotFoundStatus(status) {
+ return nil, ErrNotFoundBucket
}
bucketId := b.Id
- bucketStatus := tx.getBucketStatus(DataStructureBTree, bucket)
- if bucketStatus == BucketStatusDeleted {
- return nil, ErrBucketNotFound
- }
-
status, entry := tx.findEntryAndItsStatus(DataStructureBTree, bucket, string(key))
- if status != NotFoundEntry && entry != nil {
- if status == EntryDeleted {
- return nil, ErrKeyNotFound
- } else {
- return entry.Value, nil
- }
+ switch status {
+ case EntryDeleted:
+ return nil, ErrKeyNotFound
+ case EntryUpdated:
+ return entry.Value, nil
}
if idx, ok := tx.db.Index.bTree.exist(bucketId); ok {
@@ -124,7 +124,7 @@ func (tx *Tx) get(bucket string, key []byte) (value []byte, err error) {
return value, nil
} else {
- return nil, ErrNotFoundBucket
+ return nil, ErrKeyNotFound
}
}
@@ -146,15 +146,22 @@ func (tx *Tx) getMaxOrMinKey(bucket string, isMax bool) ([]byte, error) {
return nil, err
}
- b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
- if err != nil {
- return nil, err
+ status, b := tx.getBucketAndItsStatus(DataStructureBTree, bucket)
+ if isBucketNotFoundStatus(status) {
+ return nil, ErrNotFoundBucket
}
bucketId := b.Id
+ var (
+ key []byte = nil
+ actuallyFound = false
+ )
+
+ key, actuallyFound = tx.pendingWrites.MaxOrMinKey(bucket, isMax)
+
if idx, ok := tx.db.Index.bTree.exist(bucketId); ok {
var (
- item *Item
+ item *data.Item[data.Record]
found bool
)
@@ -165,32 +172,45 @@ func (tx *Tx) getMaxOrMinKey(bucket string, isMax bool) ([]byte, error) {
}
if !found {
+ if actuallyFound {
+ return key, nil
+ }
return nil, ErrKeyNotFound
+ } else {
+ actuallyFound = found
}
- if item.record.IsExpired() {
- tx.putDeleteLog(bucketId, item.key, nil, Persistent, DataDeleteFlag, uint64(time.Now().Unix()), DataStructureBTree)
- return nil, ErrNotFoundKey
+ if item.Record.IsExpired() {
+ tx.putDeleteLog(bucketId, item.Key, nil, Persistent, DataDeleteFlag, uint64(time.Now().Unix()), DataStructureBTree)
+ if actuallyFound {
+ return key, nil
+ }
+ return nil, ErrKeyNotFound
}
-
- return item.key, nil
- } else {
- return nil, ErrNotFoundBucket
+ if isMax {
+ key = compareAndReturn(key, item.Key, 1)
+ } else {
+ key = compareAndReturn(key, item.Key, -1)
+ }
+ }
+ if actuallyFound {
+ return key, nil
}
+ return nil, ErrKeyNotFound
}
-// GetAll returns all keys and values of the bucket stored at given bucket.
+// GetAll returns all keys and values in the given bucket.
func (tx *Tx) GetAll(bucket string) ([][]byte, [][]byte, error) {
return tx.getAllOrKeysOrValues(bucket, getAllType)
}
-// GetKeys returns all keys of the bucket stored at given bucket.
+// GetKeys returns all keys in the given bucket.
func (tx *Tx) GetKeys(bucket string) ([][]byte, error) {
keys, _, err := tx.getAllOrKeysOrValues(bucket, getKeysType)
return keys, err
}
-// GetValues returns all values of the bucket stored at given bucket.
+// GetValues returns all values in the given bucket.
func (tx *Tx) GetValues(bucket string) ([][]byte, error) {
_, values, err := tx.getAllOrKeysOrValues(bucket, getValuesType)
return values, err
@@ -206,31 +226,32 @@ func (tx *Tx) getAllOrKeysOrValues(bucket string, typ uint8) ([][]byte, [][]byte
return nil, nil, err
}
- if index, ok := tx.db.Index.bTree.exist(bucketId); ok {
- records := index.All()
+ idx, bucketExists := tx.db.Index.bTree.exist(bucketId)
+ if !bucketExists {
+ return [][]byte{}, [][]byte{}, nil
+ }
- var (
- keys [][]byte
- values [][]byte
- )
+ records := idx.All()
- switch typ {
- case getAllType:
- keys, values, err = tx.getHintIdxDataItemsWrapper(records, ScanNoLimit, bucketId, true, true)
- case getKeysType:
- keys, _, err = tx.getHintIdxDataItemsWrapper(records, ScanNoLimit, bucketId, true, false)
- case getValuesType:
- _, values, err = tx.getHintIdxDataItemsWrapper(records, ScanNoLimit, bucketId, false, true)
- }
+ var (
+ keys [][]byte
+ values [][]byte
+ )
- if err != nil {
- return nil, nil, err
- }
+ switch typ {
+ case getAllType:
+ keys, values, err = tx.getHintIdxDataItemsWrapper(records, ScanNoLimit, bucketId, true, true)
+ case getKeysType:
+ keys, _, err = tx.getHintIdxDataItemsWrapper(records, ScanNoLimit, bucketId, true, false)
+ case getValuesType:
+ _, values, err = tx.getHintIdxDataItemsWrapper(records, ScanNoLimit, bucketId, false, true)
+ }
- return keys, values, nil
+ if err != nil {
+ return nil, nil, err
}
- return nil, nil, nil
+ return keys, values, nil
}
func (tx *Tx) GetSet(bucket string, key, value []byte) (oldValue []byte, err error) {
@@ -242,90 +263,167 @@ func (tx *Tx) GetSet(bucket string, key, value []byte) (oldValue []byte, err err
})
}
-// RangeScan query a range at given bucket, start and end slice.
-func (tx *Tx) RangeScan(bucket string, start, end []byte) (values [][]byte, err error) {
+// Has returns true if the record exists. It checks without retrieving the
+// value from disk making lookups significantly faster while keeping memory
+// usage down as well. It does require the `HintKeyAndRAMIdxMode` option to be
+// enabled to function as described.
+func (tx *Tx) Has(bucket string, key []byte) (exists bool, err error) {
if err := tx.checkTxIsClosed(); err != nil {
- return nil, err
+ return false, err
}
- b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
- if err != nil {
- return nil, err
+
+ status, b := tx.getBucketAndItsStatus(DataStructureBTree, bucket)
+ if isBucketNotFoundStatus(status) {
+ return false, ErrNotFoundBucket
}
bucketId := b.Id
- if index, ok := tx.db.Index.bTree.exist(bucketId); ok {
- records := index.Range(start, end)
- if err != nil {
- return nil, ErrRangeScan
- }
+ status, _ = tx.findEntryAndItsStatus(DataStructureBTree, bucket, string(key))
+ switch status {
+ case EntryDeleted:
+ return false, ErrKeyNotFound
+ case EntryUpdated:
+ return true, nil
+ }
- _, values, err = tx.getHintIdxDataItemsWrapper(records, ScanNoLimit, bucketId, false, true)
- if err != nil {
- return nil, ErrRangeScan
- }
+ idx, bucketExists := tx.db.Index.bTree.exist(bucketId)
+ if !bucketExists {
+ return false, ErrBucketNotExist
}
+ record, recordExists := idx.Find(key)
- if len(values) == 0 {
- return nil, ErrRangeScan
+ if recordExists && !record.IsExpired() {
+ return true, nil
}
- return
+ return false, nil
}
-// PrefixScan iterates over a key prefix at given bucket, prefix and limitNum.
-// LimitNum will limit the number of entries return.
-func (tx *Tx) PrefixScan(bucket string, prefix []byte, offsetNum int, limitNum int) (values [][]byte, err error) {
+// RangeScanEntries query a range at given bucket, start and end slice. It will
+// return keys and/or values based on the includeKeys and includeValues flags.
+func (tx *Tx) RangeScanEntries(bucket string, start, end []byte, includeKeys, includeValues bool) (keys, values [][]byte, err error) {
if err := tx.checkTxIsClosed(); err != nil {
- return nil, err
+ return nil, nil, err
}
- b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
- if err != nil {
- return nil, err
+ status, b := tx.getBucketAndItsStatus(DataStructureBTree, bucket)
+ if isBucketNotFoundStatus(status) {
+ return nil, nil, ErrNotFoundBucket
}
bucketId := b.Id
+ pendingKeys, pendingValues := tx.pendingWrites.getDataByRange(start, end, b.Name)
+ if index, ok := tx.db.Index.bTree.exist(bucketId); ok {
+ records := index.Range(start, end)
- if idx, ok := tx.db.Index.bTree.exist(bucketId); ok {
- records := idx.PrefixScan(prefix, offsetNum, limitNum)
- _, values, err = tx.getHintIdxDataItemsWrapper(records, limitNum, bucketId, false, true)
- if err != nil {
- return nil, ErrPrefixScan
+ // 如果需要合并 pending,则必须拿到 keys 进行有序 merge;
+ // 否则可以按需提取,避免不必要的 keys 分配。
+ needKeysForMerge := includeKeys || len(pendingKeys) > 0
+
+ keys, values, err = tx.getHintIdxDataItemsWrapper(
+ records, ScanNoLimit, bucketId, needKeysForMerge, includeValues,
+ )
+ if err != nil && len(pendingKeys) == 0 && len(pendingValues) == 0 {
+ // If there is no item in pending and persist db,
+ // return error itself.
+ return nil, nil, err
}
}
- if len(values) == 0 {
- return nil, ErrPrefixScan
+ // 仅当存在 pending 时才进行 merge,避免 values-only 且无 pending 的场景强制构建 keys。
+ if len(pendingKeys) > 0 || len(pendingValues) > 0 {
+ keys, values = mergeKeyValues(pendingKeys, pendingValues, keys, values)
+ }
+
+ // Check for empty results before setting to nil
+ if includeKeys && len(keys) == 0 {
+ return nil, nil, ErrRangeScan
+ }
+ if includeValues && len(values) == 0 {
+ return nil, nil, ErrRangeScan
+ }
+
+ if !includeKeys {
+ keys = nil
+ }
+ if !includeValues {
+ values = nil
}
return
}
-// PrefixSearchScan iterates over a key prefix at given bucket, prefix, match regular expression and limitNum.
-// LimitNum will limit the number of entries return.
-func (tx *Tx) PrefixSearchScan(bucket string, prefix []byte, reg string, offsetNum int, limitNum int) (values [][]byte, err error) {
+// RangeScan query a range at given bucket, start and end slice.
+func (tx *Tx) RangeScan(bucket string, start, end []byte) (values [][]byte, err error) {
+ // RangeScan is kept as an API call to not break upstream projects that
+ // rely on it.
+ _, values, err = tx.RangeScanEntries(bucket, start, end, false, true)
+ return
+}
+
+// PrefixScanEntries iterates over a key prefix at given bucket, prefix and
+// limitNum. If reg is set a regular expression will be used to filter the
+// found entries. LimitNum will limit the number of entries return. It will
+// return keys and/or values based on the includeKeys and includeValues flags.
+func (tx *Tx) PrefixScanEntries(bucket string, prefix []byte, reg string, offsetNum int, limitNum int, includeKeys, includeValues bool) (keys, values [][]byte, err error) {
+ // This function is a bit awkward but that is to maintain backwards
+ // compatibility while enabling the caller to pick and choose which
+ // variation to call.
if err := tx.checkTxIsClosed(); err != nil {
- return nil, err
+ return nil, nil, err
}
b, err := tx.db.bm.GetBucket(DataStructureBTree, bucket)
if err != nil {
- return nil, err
+ return nil, nil, err
}
bucketId := b.Id
+ xerr := func(e error) error {
+ // Return expected error types based on Scan/SearchScan.
+ if reg == "" {
+ return ErrPrefixScan
+ }
+ return ErrPrefixSearchScan
+ }
+
if idx, ok := tx.db.Index.bTree.exist(bucketId); ok {
- records := idx.PrefixSearchScan(prefix, reg, offsetNum, limitNum)
- _, values, err = tx.getHintIdxDataItemsWrapper(records, limitNum, bucketId, false, true)
+ var records []*data.Record
+ if reg == "" {
+ records = idx.PrefixScan(prefix, offsetNum, limitNum)
+ } else {
+ records = idx.PrefixSearchScan(prefix, reg, offsetNum, limitNum)
+ }
+ keys, values, err = tx.getHintIdxDataItemsWrapper(records, limitNum, bucketId, includeKeys, includeValues)
if err != nil {
- return nil, ErrPrefixSearchScan
+ return nil, nil, xerr(err)
}
}
- if len(values) == 0 {
- return nil, ErrPrefixSearchScan
+ if includeKeys && len(keys) == 0 {
+ return nil, nil, xerr(err)
+ }
+ if includeValues && len(values) == 0 {
+ return nil, nil, xerr(err)
}
-
return
}
+// PrefixScan iterates over a key prefix at given bucket, prefix and limitNum.
+// LimitNum will limit the number of entries return.
+func (tx *Tx) PrefixScan(bucket string, prefix []byte, offsetNum int, limitNum int) (values [][]byte, err error) {
+ // PrefixScan is kept as an API call to not break upstream projects
+ // that rely on it.
+ _, values, err = tx.PrefixScanEntries(bucket, prefix, "", offsetNum, limitNum, false, true)
+ return values, err
+}
+
+// PrefixSearchScan iterates over a key prefix at given bucket, prefix, match regular expression and limitNum.
+// LimitNum will limit the number of entries return.
+func (tx *Tx) PrefixSearchScan(bucket string, prefix []byte, reg string, offsetNum int, limitNum int) (values [][]byte, err error) {
+ // PrefixSearchScan is kept as an API call to not break upstream projects
+ // that rely on it.
+ _, values, err = tx.PrefixScanEntries(bucket, prefix, reg, offsetNum, limitNum, false, true)
+ return values, err
+}
+
// Delete removes a key from the bucket at given bucket and key.
func (tx *Tx) Delete(bucket string, key []byte) error {
if err := tx.checkTxIsClosed(); err != nil {
@@ -337,97 +435,141 @@ func (tx *Tx) Delete(bucket string, key []byte) error {
}
bucketId := b.Id
+ //Find the key in the transaction-local map (entriesInBTree)
+ status, _ := tx.findEntryAndItsStatus(DataStructureBTree, bucket, string(key))
+ switch status {
+ case EntryDeleted:
+ return ErrKeyNotFound
+ case EntryUpdated:
+ return tx.put(bucket, key, nil, Persistent, DataDeleteFlag, uint64(time.Now().Unix()), DataStructureBTree)
+ }
+
if idx, ok := tx.db.Index.bTree.exist(bucketId); ok {
if _, found := idx.Find(key); !found {
return ErrKeyNotFound
}
} else {
- return ErrNotFoundBucket
+ return ErrKeyNotFound
}
return tx.put(bucket, key, nil, Persistent, DataDeleteFlag, uint64(time.Now().Unix()), DataStructureBTree)
}
// getHintIdxDataItemsWrapper returns keys and values when prefix scanning or range scanning.
-func (tx *Tx) getHintIdxDataItemsWrapper(records []*Record, limitNum int, bucketId BucketId, needKeys bool, needValues bool) (keys [][]byte, values [][]byte, err error) {
+func (tx *Tx) getHintIdxDataItemsWrapper(records []*data.Record, limitNum int, bucketId BucketId, needKeys bool, needValues bool) (keys [][]byte, values [][]byte, err error) {
+ // Pre-allocate capacity to reduce slice re-growth
+ estimatedSize := len(records)
+ if limitNum > 0 && limitNum < estimatedSize {
+ estimatedSize = limitNum
+ }
+
+ if needKeys {
+ keys = make([][]byte, 0, estimatedSize)
+ }
+ if needValues {
+ values = make([][]byte, 0, estimatedSize)
+ }
+
+ processedCount := 0
+ needAny := needKeys || needValues
+
for _, record := range records {
if record.IsExpired() {
tx.putDeleteLog(bucketId, record.Key, nil, Persistent, DataDeleteFlag, uint64(time.Now().Unix()), DataStructureBTree)
continue
}
- if limitNum > 0 && len(values) < limitNum || limitNum == ScanNoLimit {
- if needKeys {
- keys = append(keys, record.Key)
- }
+ // 正确的 limit 控制:与是否需要 keys/values 无关
+ if limitNum > 0 && needAny && processedCount >= limitNum {
+ break
+ }
- if needValues {
- value, err := tx.db.getValueByRecord(record)
- if err != nil {
- return nil, nil, err
- }
- values = append(values, value)
+ if needKeys {
+ keys = append(keys, record.Key)
+ }
+ if needValues {
+ value, err := tx.db.getValueByRecord(record)
+ if err != nil {
+ return nil, nil, err
}
+ values = append(values, value)
+ }
+
+ if needAny {
+ processedCount++
}
}
return keys, values, nil
}
-func (tx *Tx) tryGet(bucket string, key []byte, solveRecord func(record *Record, found bool, bucketId BucketId) error) error {
+func (tx *Tx) tryGet(bucket string, key []byte, solveRecord func(record *data.Record, entry *Entry, found bool, bucketId BucketId) error) error {
if err := tx.checkTxIsClosed(); err != nil {
return err
}
-
- bucketId, err := tx.db.bm.GetBucketID(DataStructureBTree, bucket)
- if err != nil {
- return err
+ status, b := tx.getBucketAndItsStatus(DataStructureBTree, bucket)
+ if isBucketNotFoundStatus(status) {
+ return ErrNotFoundBucket
}
-
+ bucketId := b.Id
+ var (
+ record *data.Record = nil
+ found bool
+ )
+ entry, err := tx.pendingWrites.Get(DataStructureBTree, bucket, key)
+ found = err == nil
if idx, ok := tx.db.Index.bTree.exist(bucketId); ok {
- record, found := idx.Find(key)
- return solveRecord(record, found, bucketId)
- } else {
- return ErrBucketNotFound
+ record, ok = idx.Find(key)
+ if ok {
+ found = true
+ }
}
+ return solveRecord(record, entry, found, bucketId)
}
func (tx *Tx) update(bucket string, key []byte, getNewValue func([]byte) ([]byte, error), getNewTTL func(uint32) (uint32, error)) error {
- return tx.tryGet(bucket, key, func(record *Record, found bool, bucketId BucketId) error {
+ return tx.tryGet(bucket, key, func(record *data.Record, pendingEntry *Entry, found bool, bucketId BucketId) error {
if !found {
return ErrKeyNotFound
}
- if record.IsExpired() {
- tx.putDeleteLog(bucketId, key, nil, Persistent, DataDeleteFlag, uint64(time.Now().Unix()), DataStructureBTree)
- return ErrNotFoundKey
+ if record != nil {
+ // If record is timeout, this entry should not be update to table.
+ if err := tx.revertExpiredTTLRecord(bucketId, record); err != nil {
+ return err
+ }
}
- value, err := tx.db.getValueByRecord(record)
+ value, ttl, err := tx.loadValue(record, pendingEntry)
if err != nil {
return err
}
+
newValue, err := getNewValue(value)
if err != nil {
return err
}
- newTTL, err := getNewTTL(record.TTL)
+ newTTL, err := getNewTTL(ttl)
if err != nil {
return err
}
-
- return tx.put(bucket, key, newValue, newTTL, DataSetFlag, uint64(time.Now().Unix()), DataStructureBTree)
+ return tx.put(bucket, key, newValue, newTTL, DataSetFlag, uint64(time.Now().UnixMilli()), DataStructureBTree)
})
}
func (tx *Tx) updateOrPut(bucket string, key, value []byte, getUpdatedValue func([]byte) ([]byte, error)) error {
- return tx.tryGet(bucket, key, func(record *Record, found bool, bucketId BucketId) error {
+ return tx.tryGet(bucket, key, func(record *data.Record, pendingEntry *Entry, found bool, bucketId BucketId) error {
if !found {
return tx.put(bucket, key, value, Persistent, DataSetFlag, uint64(time.Now().Unix()), DataStructureBTree)
}
- value, err := tx.db.getValueByRecord(record)
+ if record != nil {
+ if err := tx.revertExpiredTTLRecord(bucketId, record); err != nil {
+ return err
+ }
+ }
+ value, ttl, err := tx.loadValue(record, pendingEntry)
if err != nil {
return err
}
@@ -436,7 +578,7 @@ func (tx *Tx) updateOrPut(bucket string, key, value []byte, getUpdatedValue func
return err
}
- return tx.put(bucket, key, newValue, record.TTL, DataSetFlag, uint64(time.Now().Unix()), DataStructureBTree)
+ return tx.put(bucket, key, newValue, ttl, DataSetFlag, uint64(time.Now().Unix()), DataStructureBTree)
})
}
@@ -535,18 +677,24 @@ func (tx *Tx) GetTTL(bucket string, key []byte) (int64, error) {
return 0, err
}
- bucketId, err := tx.db.bm.GetBucketID(DataStructureBTree, bucket)
- if err != nil {
- return 0, err
- }
- idx, bucketExists := tx.db.Index.bTree.exist(bucketId)
-
- if !bucketExists {
+ status, b := tx.getBucketAndItsStatus(DataStructureBTree, bucket)
+ if isBucketNotFoundStatus(status) {
return 0, ErrBucketNotFound
}
+ bucketId := b.Id
- record, recordFound := idx.Find(key)
+ idx, bucketExists := tx.db.Index.bTree.exist(bucketId)
+ var (
+ record *data.Record = nil
+ recordFound bool = false
+ )
+ if pendingTTL, err := tx.pendingWrites.GetTTL(DataStructureBTree, bucket, key); err == nil {
+ return pendingTTL, nil
+ }
+ if bucketExists {
+ record, recordFound = idx.Find(key)
+ }
if !recordFound || record.IsExpired() {
return 0, ErrKeyNotFound
}
@@ -555,7 +703,7 @@ func (tx *Tx) GetTTL(bucket string, key []byte) (int64, error) {
return -1, nil
}
- remTTL := tx.db.expireTime(record.Timestamp, record.TTL)
+ remTTL := expireTime(record.Timestamp, record.TTL)
if remTTL >= 0 {
return int64(remTTL.Seconds()), nil
} else {
@@ -563,7 +711,7 @@ func (tx *Tx) GetTTL(bucket string, key []byte) (int64, error) {
}
}
-// Persist updates record's TTL as Persistent if the record exits.
+// Persist updates record's TTL as Persistent if the record exists.
func (tx *Tx) Persist(bucket string, key []byte) error {
return tx.update(bucket, key, func(oldValue []byte) ([]byte, error) {
return oldValue, nil
@@ -637,3 +785,32 @@ func (tx *Tx) GetRange(bucket string, key []byte, start, end int) ([]byte, error
return value[start : end+1], nil
}
+
+// loadValue in order to load value during pending and
+// stored items, we need this function to load value and
+// TTL.
+func (tx *Tx) loadValue(
+ rec *data.Record,
+ pendingEntry *Entry,
+) (value []byte, ttl uint32, err error) {
+
+ if rec == nil && pendingEntry == nil {
+ return nil, 0, ErrNotFoundKey
+ }
+ if pendingEntry != nil {
+ return pendingEntry.Value, pendingEntry.Meta.TTL, nil
+ }
+ return rec.Value, rec.TTL, nil
+}
+
+// revertExpiredTTLRecord revert record if it is expired.
+func (tx *Tx) revertExpiredTTLRecord(
+ bucketId BucketId,
+ rec *data.Record,
+) (err error) {
+ if rec.IsExpired() {
+ tx.putDeleteLog(bucketId, rec.Key, nil, Persistent, DataDeleteFlag, uint64(time.Now().Unix()), DataStructureBTree)
+ return ErrNotFoundKey
+ }
+ return nil
+}
diff --git a/vendor/github.com/nutsdb/nutsdb/tx_bucket.go b/vendor/github.com/nutsdb/nutsdb/tx_bucket.go
index b76bc987ba..d5fb9ccd37 100644
--- a/vendor/github.com/nutsdb/nutsdb/tx_bucket.go
+++ b/vendor/github.com/nutsdb/nutsdb/tx_bucket.go
@@ -14,6 +14,8 @@
package nutsdb
+import "github.com/nutsdb/nutsdb/internal/utils"
+
// IterateBuckets iterate over all the bucket depends on ds (represents the data structure)
func (tx *Tx) IterateBuckets(ds uint16, pattern string, f func(bucket string) bool) error {
if err := tx.checkTxIsClosed(); err != nil {
@@ -26,7 +28,7 @@ func (tx *Tx) IterateBuckets(ds uint16, pattern string, f func(bucket string) bo
if err != nil {
return err
}
- if end, err := MatchForRange(pattern, bucket.Name, f); end || err != nil {
+ if end, err := utils.MatchForRange(pattern, bucket.Name, f); end || err != nil {
return err
}
}
@@ -37,7 +39,7 @@ func (tx *Tx) IterateBuckets(ds uint16, pattern string, f func(bucket string) bo
if err != nil {
return err
}
- if end, err := MatchForRange(pattern, bucket.Name, f); end || err != nil {
+ if end, err := utils.MatchForRange(pattern, bucket.Name, f); end || err != nil {
return err
}
}
@@ -48,7 +50,7 @@ func (tx *Tx) IterateBuckets(ds uint16, pattern string, f func(bucket string) bo
if err != nil {
return err
}
- if end, err := MatchForRange(pattern, bucket.Name, f); end || err != nil {
+ if end, err := utils.MatchForRange(pattern, bucket.Name, f); end || err != nil {
return err
}
}
@@ -59,7 +61,7 @@ func (tx *Tx) IterateBuckets(ds uint16, pattern string, f func(bucket string) bo
if err != nil {
return err
}
- if end, err := MatchForRange(pattern, bucket.Name, f); end || err != nil {
+ if end, err := utils.MatchForRange(pattern, bucket.Name, f); end || err != nil {
return err
}
}
diff --git a/vendor/github.com/nutsdb/nutsdb/tx_error.go b/vendor/github.com/nutsdb/nutsdb/tx_error.go
index dac12b90a6..9036064dcf 100644
--- a/vendor/github.com/nutsdb/nutsdb/tx_error.go
+++ b/vendor/github.com/nutsdb/nutsdb/tx_error.go
@@ -1,6 +1,8 @@
package nutsdb
-import "errors"
+import (
+ "errors"
+)
var (
// ErrDataSizeExceed is returned when given key and value size is too big.
@@ -31,6 +33,7 @@ var (
// ErrNotFoundKey is returned when key not found int the bucket on an view function.
ErrNotFoundKey = errors.New("key not found in the bucket")
+ ErrKeyNotFound = ErrNotFoundKey
// ErrCannotCommitAClosedTx is returned when the tx committing a closed tx
ErrCannotCommitAClosedTx = errors.New("can not commit a closed tx")
@@ -40,19 +43,17 @@ var (
ErrCannotRollbackAClosedTx = errors.New("can not rollback a closed tx")
- // ErrNotFoundBucket is returned when key not found int the bucket on an view function.
+ // ErrNotFoundBucket is returned when bucket not found int the bucket on an view function.
ErrNotFoundBucket = errors.New("bucket not found")
// ErrTxnTooBig is returned if too many writes are fit into a single transaction.
- ErrTxnTooBig = errors.New("Txn is too big to fit into one request")
+ ErrTxnTooBig = errors.New("txn is too big to fit into one request")
// ErrTxnExceedWriteLimit is returned when this tx's write is exceed max write record
ErrTxnExceedWriteLimit = errors.New("txn is exceed max write record count")
ErrBucketAlreadyExist = errors.New("bucket is already exist")
- ErrorBucketNotExist = errors.New("bucket is not exist yet, please use NewBucket function to create this bucket first")
-
ErrValueNotInteger = errors.New("value is not an integer")
ErrOffsetInvalid = errors.New("offset is invalid")
@@ -60,4 +61,6 @@ var (
ErrKVArgsLenNotEven = errors.New("parameters is used to represent key value pairs and cannot be odd numbers")
ErrStartGreaterThanEnd = errors.New("start is greater than end")
+
+ ErrInvalidKey = errors.New("invalid key")
)
diff --git a/vendor/github.com/nutsdb/nutsdb/tx_list.go b/vendor/github.com/nutsdb/nutsdb/tx_list.go
index 924654203c..146853a9c7 100644
--- a/vendor/github.com/nutsdb/nutsdb/tx_list.go
+++ b/vendor/github.com/nutsdb/nutsdb/tx_list.go
@@ -20,18 +20,32 @@ import (
"strings"
"time"
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/utils"
"github.com/pkg/errors"
"github.com/xujiajun/utils/strconv2"
)
-var bucketKeySeqMap map[string]*HeadTailSeq
-
-// ErrSeparatorForListKey returns when list key contains the SeparatorForListKey.
-var ErrSeparatorForListKey = errors.Errorf("contain separator (%s) for List key", SeparatorForListKey)
-
// SeparatorForListKey represents separator for listKey
const SeparatorForListKey = "|"
+var (
+ // ErrListNotFound is returned when the list not found.
+ ErrListNotFound = data.ErrListNotFound
+
+ // ErrCount is returned when count is error.
+ ErrCount = data.ErrCount
+
+ // ErrEmptyList is returned when the list is empty.
+ ErrEmptyList = data.ErrEmptyList
+
+ // ErrStartOrEnd is returned when start > end
+ ErrStartOrEnd = data.ErrStartOrEnd
+
+ // ErrSeparatorForListKey returns when list key contains the SeparatorForListKey.
+ ErrSeparatorForListKey = errors.Errorf("contain separator (%s) for List key", SeparatorForListKey)
+)
+
// RPop removes and returns the last element of the list stored in the bucket at given bucket and key.
func (tx *Tx) RPop(bucket string, key []byte) (item []byte, err error) {
item, err = tx.RPeek(bucket, key)
@@ -54,7 +68,7 @@ func (tx *Tx) RPeek(bucket string, key []byte) ([]byte, error) {
}
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
@@ -71,7 +85,7 @@ func (tx *Tx) RPeek(bucket string, key []byte) ([]byte, error) {
return nil, err
}
- v, err := tx.db.getValueByRecord(item.record)
+ v, err := tx.db.getValueByRecord(item.Record)
if err != nil {
return nil, err
}
@@ -91,24 +105,18 @@ func (tx *Tx) push(bucket string, key []byte, flag uint16, values ...[]byte) err
return nil
}
-func (tx *Tx) getListNewKey(bucket string, key []byte, isLeft bool) []byte {
- if bucketKeySeqMap == nil {
- bucketKeySeqMap = make(map[string]*HeadTailSeq)
- }
-
+// getListWithDefault
+// this function will get list, if list not exists, will create
+// a new one.
+func (tx *Tx) getListWithDefault(bucket string) (*data.List, error) {
b, err := tx.db.bm.GetBucket(DataStructureList, bucket)
if err != nil {
- return nil
+ return nil, err
}
bucketId := b.Id
- bucketKey := bucket + string(key)
- if _, ok := bucketKeySeqMap[bucketKey]; !ok {
- bucketKeySeqMap[bucketKey] = tx.getListHeadTailSeq(bucketId, string(key))
- }
-
- seq := generateSeq(bucketKeySeqMap[bucketKey], isLeft)
- return encodeListKey(key, seq)
+ // 确保列表索引存在
+ return tx.db.Index.list.getWithDefault(bucketId), nil
}
// RPush inserts the values at the tail of the list stored in the bucket at given bucket,key and values.
@@ -122,8 +130,12 @@ func (tx *Tx) RPush(bucket string, key []byte, values ...[]byte) error {
}
for _, value := range values {
- newKey := tx.getListNewKey(bucket, key, false)
- err := tx.push(bucket, newKey, DataLPushFlag, value)
+ l, err := tx.getListWithDefault(bucket)
+ if err != nil {
+ return err
+ }
+ newKey := l.GeneratePushKey(key, false)
+ err = tx.push(bucket, newKey, DataRPushFlag, value)
if err != nil {
return err
}
@@ -143,8 +155,12 @@ func (tx *Tx) LPush(bucket string, key []byte, values ...[]byte) error {
}
for _, value := range values {
- newKey := tx.getListNewKey(bucket, key, true)
- err := tx.push(bucket, newKey, DataLPushFlag, value)
+ l, err := tx.getListWithDefault(bucket)
+ if err != nil {
+ return err
+ }
+ newKey := l.GeneratePushKey(key, true)
+ err = tx.push(bucket, newKey, DataLPushFlag, value)
if err != nil {
return err
}
@@ -203,7 +219,7 @@ func (tx *Tx) LPeek(bucket string, key []byte) (item []byte, err error) {
}
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
@@ -218,7 +234,7 @@ func (tx *Tx) LPeek(bucket string, key []byte) (item []byte, err error) {
return nil, err
}
- v, err := tx.db.getValueByRecord(r.record)
+ v, err := tx.db.getValueByRecord(r.Record)
if err != nil {
return nil, err
}
@@ -239,7 +255,7 @@ func (tx *Tx) LSize(bucket string, key []byte) (int, error) {
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
@@ -268,7 +284,7 @@ func (tx *Tx) LRange(bucket string, key []byte, start, end int) ([][]byte, error
}
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
@@ -351,7 +367,7 @@ func (tx *Tx) LTrim(bucket string, key []byte, start, end int) error {
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
@@ -402,7 +418,7 @@ func (tx *Tx) LRemByIndex(bucket string, key []byte, indexes ...int) error {
}
sort.Ints(indexes)
- data, err := MarshalInts(indexes)
+ data, err := utils.MarshalInts(indexes)
if err != nil {
return err
}
@@ -426,7 +442,7 @@ func (tx *Tx) LKeys(bucket, pattern string, f func(key string) bool) error {
}
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
if l, exist = tx.db.Index.list.exist(bucketId); !exist {
@@ -437,7 +453,7 @@ func (tx *Tx) LKeys(bucket, pattern string, f func(key string) bool) error {
if tx.CheckExpire(bucket, []byte(key)) {
continue
}
- if end, err := MatchForRange(pattern, key, f); end || err != nil {
+ if end, err := utils.MatchForRange(pattern, key, f); end || err != nil {
return err
}
}
@@ -455,7 +471,7 @@ func (tx *Tx) ExpireList(bucket string, key []byte, ttl uint32) error {
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
@@ -463,8 +479,7 @@ func (tx *Tx) ExpireList(bucket string, key []byte, ttl uint32) error {
return ErrBucket
}
- l.TTL[string(key)] = ttl
- l.TimeStamp[string(key)] = uint64(time.Now().Unix())
+ l.ExpireList(key, ttl)
ttls := strconv2.Int64ToStr(int64(ttl))
err = tx.push(bucket, key, DataExpireListFlag, []byte(ttls))
if err != nil {
@@ -481,7 +496,7 @@ func (tx *Tx) CheckExpire(bucket string, key []byte) bool {
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
@@ -507,7 +522,7 @@ func (tx *Tx) GetListTTL(bucket string, key []byte) (uint32, error) {
var (
bucketId = b.Id
- l *List
+ l *data.List
exist bool
)
if l, exist = tx.db.Index.list.exist(bucketId); !exist {
diff --git a/vendor/github.com/nutsdb/nutsdb/tx_set.go b/vendor/github.com/nutsdb/nutsdb/tx_set.go
index 81356de566..966569ca55 100644
--- a/vendor/github.com/nutsdb/nutsdb/tx_set.go
+++ b/vendor/github.com/nutsdb/nutsdb/tx_set.go
@@ -17,9 +17,22 @@ package nutsdb
import (
"time"
+ "github.com/nutsdb/nutsdb/internal/data"
+ "github.com/nutsdb/nutsdb/internal/utils"
"github.com/pkg/errors"
)
+var (
+ // ErrSetNotExist is returned when the key does not exist.
+ ErrSetNotExist = data.ErrSetNotExist
+
+ // ErrSetMemberNotExist is returned when the member of set does not exist
+ ErrSetMemberNotExist = data.ErrSetMemberNotExist
+
+ // ErrMemberEmpty is returned when the item received is nil
+ ErrMemberEmpty = data.ErrMemberEmpty
+)
+
func (tx *Tx) sPut(bucket string, key []byte, dataFlag uint16, values ...[]byte) error {
if dataFlag == DataSetFlag {
@@ -43,7 +56,7 @@ func (tx *Tx) sPut(bucket string, key []byte, dataFlag uint16, values ...[]byte)
}
for _, value := range values {
- hash, err := getFnv32(value)
+ hash, err := utils.GetFnv32(value)
if err != nil {
return err
}
@@ -272,7 +285,7 @@ func (tx *Tx) SDiffByTwoBuckets(bucket1 string, key1 []byte, bucket2 string, key
}
var (
- set1, set2 *Set
+ set1, set2 *data.Set
ok bool
)
@@ -336,7 +349,7 @@ func (tx *Tx) SMoveByTwoBuckets(bucket1 string, key1 []byte, bucket2 string, key
}
var (
- set1, set2 *Set
+ set1, set2 *data.Set
ok bool
)
@@ -368,13 +381,13 @@ func (tx *Tx) SMoveByTwoBuckets(bucket1 string, key1 []byte, bucket2 string, key
return false, ErrNotFoundKeyInBucket(bucket2, key2)
}
- hash, err := getFnv32(item)
+ hash, err := utils.GetFnv32(item)
if err != nil {
return false, err
}
if r, ok := set2.M[string(key2)][hash]; !ok {
- err := set2.SAdd(string(key2), [][]byte{item}, []*Record{r})
+ err := set2.SAdd(string(key2), [][]byte{item}, []*data.Record{r})
if err != nil {
return false, err
}
@@ -427,7 +440,7 @@ func (tx *Tx) SUnionByTwoBuckets(bucket1 string, key1 []byte, bucket2 string, ke
}
var (
- set1, set2 *Set
+ set1, set2 *data.Set
ok bool
)
b1, err := tx.db.bm.GetBucket(DataStructureSet, bucket1)
@@ -495,7 +508,7 @@ func (tx *Tx) SKeys(bucket, pattern string, f func(key string) bool) error {
return ErrBucket
} else {
for key := range set.M {
- if end, err := MatchForRange(pattern, key, f); end || err != nil {
+ if end, err := utils.MatchForRange(pattern, key, f); end || err != nil {
return err
}
}
diff --git a/vendor/github.com/nutsdb/nutsdb/tx_zset.go b/vendor/github.com/nutsdb/nutsdb/tx_zset.go
index 277f1108a5..5ea5f012a5 100644
--- a/vendor/github.com/nutsdb/nutsdb/tx_zset.go
+++ b/vendor/github.com/nutsdb/nutsdb/tx_zset.go
@@ -21,6 +21,7 @@ import (
"strings"
"time"
+ "github.com/nutsdb/nutsdb/internal/utils"
"github.com/xujiajun/utils/strconv2"
)
@@ -495,7 +496,7 @@ func (tx *Tx) ZKeys(bucket, pattern string, f func(key string) bool) error {
}
for key := range sortedSet.M {
- if end, err := MatchForRange(pattern, key, f); end || err != nil {
+ if end, err := utils.MatchForRange(pattern, key, f); end || err != nil {
return err
}
}
diff --git a/vendor/github.com/nutsdb/nutsdb/utils.go b/vendor/github.com/nutsdb/nutsdb/utils.go
index 5a452da41a..62846ea86b 100644
--- a/vendor/github.com/nutsdb/nutsdb/utils.go
+++ b/vendor/github.com/nutsdb/nutsdb/utils.go
@@ -17,86 +17,22 @@ package nutsdb
import (
"bytes"
"encoding/binary"
- "errors"
- "io"
- "os"
+ "fmt"
"path/filepath"
"strings"
+ "time"
"github.com/xujiajun/utils/strconv2"
)
-// Truncate changes the size of the file.
-func Truncate(path string, capacity int64, f *os.File) error {
- fileInfo, _ := os.Stat(path)
- if fileInfo.Size() < capacity {
- if err := f.Truncate(capacity); err != nil {
- return err
- }
- }
- return nil
-}
-
-func ConvertBigEndianBytesToUint64(data []byte) uint64 {
- return binary.BigEndian.Uint64(data)
-}
-
-func ConvertUint64ToBigEndianBytes(value uint64) []byte {
- b := make([]byte, 8)
- binary.BigEndian.PutUint64(b, value)
- return b
-}
-
-func MarshalInts(ints []int) ([]byte, error) {
- buffer := bytes.NewBuffer([]byte{})
- for _, x := range ints {
- if err := binary.Write(buffer, binary.LittleEndian, int64(x)); err != nil {
- return nil, err
- }
- }
- return buffer.Bytes(), nil
-}
-
-func UnmarshalInts(data []byte) ([]int, error) {
- var ints []int
- buffer := bytes.NewBuffer(data)
- for {
- var i int64
- err := binary.Read(buffer, binary.LittleEndian, &i)
- if errors.Is(err, io.EOF) {
- break
- } else if err != nil {
- return nil, err
- }
- ints = append(ints, int(i))
- }
- return ints, nil
-}
-
-func MatchForRange(pattern, bucket string, f func(bucket string) bool) (end bool, err error) {
- match, err := filepath.Match(pattern, bucket)
- if err != nil {
- return true, err
- }
- if match && !f(bucket) {
- return true, nil
- }
- return false, nil
-}
-
// getDataPath returns the data path for the given file ID.
func getDataPath(fID int64, dir string) string {
separator := string(filepath.Separator)
- return dir + separator + strconv2.Int64ToStr(fID) + DataSuffix
-}
-
-func OneOfUint16Array(value uint16, array []uint16) bool {
- for _, v := range array {
- if v == value {
- return true
- }
+ if IsMergeFile(fID) {
+ seq := GetMergeSeq(fID)
+ return dir + separator + fmt.Sprintf("merge_%d%s", seq, DataSuffix)
}
- return false
+ return dir + separator + strconv2.Int64ToStr(fID) + DataSuffix
}
func splitIntStringStr(str, separator string) (int, string) {
@@ -120,13 +56,6 @@ func splitIntIntStr(str, separator string) (int, int) {
return firstItem, secondItem
}
-func encodeListKey(key []byte, seq uint64) []byte {
- buf := make([]byte, len(key)+8)
- binary.LittleEndian.PutUint64(buf[:8], seq)
- copy(buf[8:], key[:])
- return buf
-}
-
func decodeListKey(buf []byte) ([]byte, uint64) {
seq := binary.LittleEndian.Uint64(buf[:8])
key := make([]byte, len(buf[8:]))
@@ -141,40 +70,84 @@ func splitStringFloat64Str(str, separator string) (string, float64) {
return firstItem, secondItem
}
-func getFnv32(value []byte) (uint32, error) {
- _, err := fnvHash.Write(value)
- if err != nil {
- return 0, err
- }
- hash := fnvHash.Sum32()
- fnvHash.Reset()
- return hash, nil
+func createNewBufferWithSize(size int) *bytes.Buffer {
+ buf := new(bytes.Buffer)
+ buf.Grow(int(size))
+ return buf
}
-func generateSeq(seq *HeadTailSeq, isLeft bool) uint64 {
- var res uint64
- if isLeft {
- res = seq.Head
- seq.Head--
- } else {
- res = seq.Tail
- seq.Tail++
+// compareAndReturn use bytes.Compare(other, target), if return value is
+// comVal, return other, else return target.
+func compareAndReturn(target []byte, other []byte, cmpVal int) []byte {
+ if target == nil {
+ return other
+ }
+ if bytes.Compare(other, target) == cmpVal {
+ return other
}
+ return target
+}
+
+func expireTime(timestamp uint64, ttl uint32) time.Duration {
+ now := time.UnixMilli(time.Now().UnixMilli())
+ expireTime := time.UnixMilli(int64(timestamp))
+ expireTime = expireTime.Add(time.Duration(int64(ttl)) * time.Second)
+ return expireTime.Sub(now)
+}
+
+func mergeKeyValues(
+ k0, v0 [][]byte,
+ k1, v1 [][]byte,
+) (keys, values [][]byte) {
+ l0 := len(k0)
+ l1 := len(k1)
+ // Pre-allocate capacity with estimated maximum size to reduce slice re-growth
+ estimatedSize := l0 + l1
+ keys = make([][]byte, 0, estimatedSize)
+ values = make([][]byte, 0, estimatedSize)
+ cur0 := 0
+ cur1 := 0
+ for cur0 < l0 && cur1 < l1 {
+ if bytes.Compare(k0[cur0], k1[cur1]) <= 0 {
+ keys = append(keys, k0[cur0])
+ values = append(values, v0[cur0])
+ cur0++
+ if bytes.Equal(k0[cur0], k1[cur1]) {
+ // skip k1 item if k0 == k1
+ cur1++
+ }
+ } else {
+ keys = append(keys, k1[cur1])
+ values = append(values, v1[cur1])
+ cur1++
+ }
+ }
+ for cur0 < l0 {
+ keys = append(keys, k0[cur0])
+ values = append(values, v0[cur0])
+ cur0++
+ }
+ for cur1 < l1 {
+ keys = append(keys, k1[cur1])
+ values = append(values, v1[cur1])
+ cur1++
+ }
+ return
+}
- return res
+// This Type is for sort a pair of (k, v).
+type sortkv struct {
+ k, v [][]byte
}
-func createNewBufferWithSize(size int) *bytes.Buffer {
- buf := new(bytes.Buffer)
- buf.Grow(int(size))
- return buf
+func (skv *sortkv) Len() int {
+ return len(skv.k)
}
-func UvarintSize(x uint64) int {
- i := 0
- for x >= 0x80 {
- x >>= 7
- i++
- }
- return i + 1
+func (skv *sortkv) Swap(i, j int) {
+ skv.k[i], skv.v[i], skv.k[j], skv.v[j] = skv.k[j], skv.v[j], skv.k[i], skv.v[i]
+}
+
+func (skv *sortkv) Less(i, j int) bool {
+ return bytes.Compare(skv.k[i], skv.k[j]) < 0
}
diff --git a/vendor/github.com/nutsdb/nutsdb/watch_manager.go b/vendor/github.com/nutsdb/nutsdb/watch_manager.go
new file mode 100644
index 0000000000..48a9e06573
--- /dev/null
+++ b/vendor/github.com/nutsdb/nutsdb/watch_manager.go
@@ -0,0 +1,585 @@
+package nutsdb
+
+import (
+ "context"
+ "errors"
+ "log"
+ "sync"
+ "sync/atomic"
+ "time"
+)
+
+// errors
+var (
+ ErrBucketSubscriberNotFound = errors.New("bucket subscriber not found")
+ ErrKeySubscriberNotFound = errors.New("key subscriber not found")
+ ErrSubscriberNotFound = errors.New("subscriber not found")
+ ErrWatchChanCannotSend = errors.New("watch channel cannot send")
+ ErrKeyAlreadySubscribed = errors.New("key already subscribed")
+ ErrWatchManagerClosed = errors.New("watch manager closed")
+ ErrWatchingCallbackFailed = errors.New("watching callback failed")
+ ErrWatchingChannelClosed = errors.New("watching channel closed")
+ ErrChannelNotAvailable = errors.New("channel not available")
+)
+
+// convert these variables to var for testing
+var (
+ watchChanBufferSize = 1024
+ receiveChanBufferSize = 1024
+ maxBatchSize = 1024
+ deadMessageThreshold = 100
+ distributeChanBufferSize = 128
+ victimBucketBufferSize = 128
+)
+
+const (
+ DefaultCallbackTimeout = 1 * time.Second
+)
+
+// message priority
+const (
+ MessagePriorityHigh = iota // the messages must be ensured to deliver to distributor
+ MessagePriorityMedium = 1 // the messages may be dropped
+)
+
+type (
+ bucketToSubscribers map[BucketName]map[string]map[uint64]*subscriber
+ victimBucketToSubscribers map[uint64]map[string]map[uint64]*subscriber
+ victimBucketChan chan victimBucketToSubscribers
+ MessagePriority int
+
+ WatchOptions struct {
+ CallbackTimeout time.Duration
+ }
+
+ MessageOptions struct {
+ Priority MessagePriority
+ }
+)
+
+type Message struct {
+ BucketName BucketName
+ Key string
+ Value []byte
+ Flag DataFlag
+ Timestamp uint64
+ priority MessagePriority
+}
+
+func NewMessage(bucketName BucketName, key string, value []byte, flag DataFlag, timestamp uint64, options ...MessageOptions) *Message {
+ var priority MessagePriority
+ // default priority is medium
+ priority = MessagePriorityMedium
+
+ if len(options) > 0 {
+ priority = options[0].Priority
+ }
+
+ return &Message{
+ BucketName: bucketName,
+ Key: key,
+ Value: value,
+ Flag: flag,
+ Timestamp: timestamp,
+ priority: priority,
+ }
+}
+
+func NewWatchOptions() *WatchOptions {
+ return &WatchOptions{
+ CallbackTimeout: DefaultCallbackTimeout,
+ }
+}
+
+// WithCallbackTimeout sets the callback timeout
+func (opts *WatchOptions) WithCallbackTimeout(timeout time.Duration) {
+ opts.CallbackTimeout = timeout
+}
+
+type subscriber struct {
+ id uint64
+ bucketName BucketName
+ key string
+ receiveChan chan *Message
+ deadMessages int
+ active atomic.Bool
+}
+
+type watchManager struct {
+ lookup bucketToSubscribers // bucketName -> key -> id -> subscriber
+ watchChan chan *Message // the hub channel to receive the messages
+ distributeChan chan []*Message // the collector worker collects messages from watchChan and sends them to the distributor worker through distributeChan
+ victimMaps victimBucketToSubscribers
+ victimChan victimBucketChan
+ workerCtx context.Context // cancellation for in-flight tasks
+ workerCancel context.CancelFunc
+ wg sync.WaitGroup
+
+ closed atomic.Bool
+ mu sync.Mutex
+ idGenerator *IDGenerator
+}
+
+func NewWatchManager() *watchManager {
+ ctx := context.Background()
+ workerCtx, workerCancel := context.WithCancel(ctx)
+ return &watchManager{
+ lookup: make(bucketToSubscribers),
+ watchChan: make(chan *Message, watchChanBufferSize),
+ distributeChan: make(chan []*Message, distributeChanBufferSize),
+ closed: atomic.Bool{},
+ workerCtx: workerCtx,
+ workerCancel: workerCancel,
+ idGenerator: &IDGenerator{currentMaxId: 0},
+ victimMaps: make(victimBucketToSubscribers),
+ victimChan: make(victimBucketChan, victimBucketBufferSize),
+ }
+}
+
+// send a message to the watch manager
+func (wm *watchManager) sendMessage(message *Message) error {
+ if wm.closed.Load() {
+ return ErrWatchManagerClosed
+ }
+
+ // the high priority messages must be ensured to push to the watch channel
+ if message.priority == MessagePriorityHigh {
+ select {
+ case wm.watchChan <- message:
+ log.Printf("[watch_manager] Sent high priority message %s/%s to watch channel\n", message.BucketName, message.Key)
+ case <-wm.workerCtx.Done():
+ return ErrWatchManagerClosed
+ }
+ }
+
+ select {
+ case wm.watchChan <- message:
+ return nil
+ case <-wm.workerCtx.Done():
+ return ErrWatchManagerClosed
+ default:
+ return ErrWatchChanCannotSend
+ }
+}
+
+func (wm *watchManager) sendUpdatedEntries(entries []*Entry, deletedbuckets map[BucketName]bool, getBucketName func(bucketId BucketId) (BucketName, error)) error {
+ if wm.closed.Load() {
+ return ErrWatchManagerClosed
+ }
+
+ // send all updated entries to the watch manager
+ if len(entries) > 0 {
+ for _, entry := range entries {
+ bucketName, err := getBucketName(entry.Meta.BucketId)
+ if err != nil {
+ continue
+ }
+
+ rawKey, err := entry.getRawKey()
+ if err != nil {
+ log.Printf("get raw key %+v error: %+v", entry.Key, err)
+ continue
+ }
+
+ message := NewMessage(bucketName, string(rawKey), entry.Value, entry.Meta.Flag, entry.Meta.Timestamp)
+ if err := wm.sendMessage(message); err != nil {
+ return err
+ }
+ }
+ }
+
+ //
+ for bucketName := range deletedbuckets {
+ message := NewMessage(bucketName, "", nil, DataBucketDeleteFlag, uint64(time.Now().Unix()), MessageOptions{Priority: MessagePriorityHigh})
+ if err := wm.sendMessage(message); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// startDistributor starts both the collector and distributor goroutines
+func (wm *watchManager) startDistributor() {
+ defer wm.cleanUpSubscribers()
+
+ // start the victim collector goroutine
+ // it collects the victim buckets from the victim channel
+ // and handle delete bucket operation
+ wm.wg.Add(1)
+ go func() {
+ defer wm.wg.Done()
+ wm.runVictimCollector()
+ }()
+
+ // Start the distributor goroutine (consumes from distributeChan)
+ wm.wg.Add(1)
+ go func() {
+ defer wm.wg.Done()
+ wm.runDistributor()
+ }()
+
+ // start the collector goroutine (collects messages into batches)
+ wm.wg.Add(1)
+ go func() {
+ defer wm.wg.Done()
+ wm.runCollector()
+ }()
+
+ wm.wg.Wait()
+}
+
+// runCollector collects messages from watchChan and batches them
+func (wm *watchManager) runCollector() {
+ batches := make([]*Message, 0, maxBatchSize)
+
+ defer func() {
+ // drain and send final batch before exiting
+ if len(batches) > 0 {
+ select {
+ case wm.distributeChan <- batches:
+ default:
+ log.Printf("[watch_manager] Dropping final batch of %d messages\n", len(batches))
+ }
+ }
+
+ close(wm.distributeChan)
+ close(wm.watchChan)
+ }()
+
+ sendBatchToDistributor := func(batch []*Message) {
+ sendBatch := make([]*Message, len(batch))
+ copy(sendBatch, batch)
+
+ select {
+ case wm.distributeChan <- sendBatch:
+ case <-wm.workerCtx.Done():
+ default:
+ log.Printf("[watch_manager] Distribution channel full, dropping batch of %d messages\n", len(sendBatch))
+ }
+ }
+
+ for {
+ select {
+ case msg, ok := <-wm.watchChan:
+ if !ok {
+ return
+ }
+ batches = append(batches, msg)
+ case <-wm.workerCtx.Done():
+ return
+ }
+
+ accumulate:
+ for {
+ if len(batches) >= maxBatchSize {
+ sendBatchToDistributor(batches)
+ batches = batches[:0]
+ break accumulate
+ }
+
+ select {
+ case msg, ok := <-wm.watchChan:
+ if !ok {
+ return
+ }
+ batches = append(batches, msg)
+ case <-wm.workerCtx.Done():
+ return
+
+ default:
+ if len(batches) > 0 {
+ sendBatchToDistributor(batches)
+ batches = batches[:0]
+ }
+ break accumulate
+ }
+ }
+ }
+}
+
+// runDistributor distributes batches to subscribers
+func (wm *watchManager) runDistributor() {
+ for {
+ select {
+ case batch, ok := <-wm.distributeChan:
+ if !ok {
+ return
+ }
+ wm.distributeAllMessages(batch)
+
+ case <-wm.workerCtx.Done():
+ // drain the distribute channel
+ for {
+ select {
+ case batch, ok := <-wm.distributeChan:
+ if !ok {
+ return
+ }
+ wm.distributeAllMessages(batch)
+ default:
+ return
+ }
+ }
+ }
+ }
+}
+
+// runVictimCollector collects the victim buckets from the victim channel
+// and handle delete bucket operation
+// The bucket is deleted only when its all ds bucket are deleted
+// we will send the delete bucket message to the subscribers when the bucket is deleted
+func (wm *watchManager) runVictimCollector() {
+ ticker := time.NewTicker(100 * time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case victimBucketToSubscribers, ok := <-wm.victimChan:
+ if !ok {
+ return
+ }
+
+ for identifierId, bucketMap := range victimBucketToSubscribers {
+ for key, keyMap := range bucketMap {
+ for _, subscriber := range keyMap {
+ if subscriber.active.Load() {
+ message := NewMessage(subscriber.bucketName, subscriber.key, nil, DataBucketDeleteFlag, uint64(time.Now().Unix()))
+ select {
+ case subscriber.receiveChan <- message:
+ default:
+ // drop the message
+ }
+ close(subscriber.receiveChan)
+ subscriber.active.Store(false)
+ }
+ delete(keyMap, subscriber.id)
+ }
+ delete(bucketMap, key)
+ }
+ delete(victimBucketToSubscribers, identifierId)
+ }
+ case <-ticker.C:
+ // avoid busy spinning
+ case <-wm.workerCtx.Done():
+ return
+ }
+ }
+}
+
+// distribute the messages to the subscribers
+func (wm *watchManager) distributeAllMessages(messages []*Message) error {
+ wm.mu.Lock()
+ defer wm.mu.Unlock()
+
+ if len(messages) == 0 {
+ return nil
+ }
+
+ dropMessage := func(message *Message, subscriber *subscriber) {
+ log.Printf("[watch_manager] Force-unsubscribing slow subscriber with id %d for message %s/%s\n",
+ subscriber.id, message.BucketName, message.Key)
+
+ if _, err := wm.findSubscriber(message.BucketName, message.Key, subscriber.id); err == nil {
+ delete(wm.lookup[message.BucketName][message.Key], subscriber.id)
+ if len(wm.lookup[message.BucketName][message.Key]) == 0 {
+ delete(wm.lookup[message.BucketName], message.Key)
+ }
+ if len(wm.lookup[message.BucketName]) == 0 {
+ delete(wm.lookup, message.BucketName)
+ }
+ if subscriber.active.Load() {
+ close(subscriber.receiveChan)
+ subscriber.active.Store(false)
+ }
+ }
+ }
+
+ for _, message := range messages {
+ bucketMap, ok := wm.lookup[message.BucketName]
+ if !ok {
+ continue
+ }
+
+ if message.Flag == DataBucketDeleteFlag {
+ // delete the bucket from the lookup
+ wm.deleteBucket(*message)
+ continue
+ }
+
+ key := message.Key
+ subscriberMap, ok := bucketMap[key]
+ if !ok {
+ continue
+ }
+
+ // avoid blocking the distributor, all messages blocked will be dropped
+ for _, subscriber := range subscriberMap {
+ if !subscriber.active.Load() {
+ log.Printf("[watch_manager] Skipping inactive subscriber with id %d for message %s/%s\n", subscriber.id, message.BucketName, message.Key)
+ continue
+ }
+
+ select {
+ case subscriber.receiveChan <- message:
+ subscriber.deadMessages = 0
+ default:
+ // when the messages are not pushed to dropChan, we consider it as dead
+ subscriber.deadMessages++
+ if subscriber.deadMessages >= deadMessageThreshold {
+ dropMessage(message, subscriber)
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// subscribe to the key and bucket
+// each subscriber has a own channel to receive messages
+func (wm *watchManager) subscribe(bucketName BucketName, key string) (*subscriber, error) {
+ if wm.isClosed() {
+ return nil, ErrWatchManagerClosed
+ }
+
+ wm.mu.Lock()
+ defer wm.mu.Unlock()
+ if _, ok := wm.lookup[bucketName]; !ok {
+ wm.lookup[bucketName] = make(map[string]map[uint64]*subscriber)
+ }
+
+ receiveChan := make(chan *Message, receiveChanBufferSize)
+
+ if _, ok := wm.lookup[bucketName][key]; !ok {
+ wm.lookup[bucketName][key] = make(map[uint64]*subscriber)
+ }
+
+ id := wm.idGenerator.GenId()
+ registeredSubscriber := subscriber{
+ id: id,
+ bucketName: bucketName,
+ key: key,
+ receiveChan: receiveChan,
+ active: atomic.Bool{},
+ }
+ registeredSubscriber.active.Store(true)
+
+ wm.lookup[bucketName][key][id] = ®isteredSubscriber
+
+ return ®isteredSubscriber, nil
+}
+
+// unsubscribe from the key and bucket
+func (wm *watchManager) unsubscribe(bucketName BucketName, key string, id BucketId) error {
+ wm.mu.Lock()
+ defer wm.mu.Unlock()
+
+ subscriber, err := wm.findSubscriber(bucketName, key, id)
+ if err != nil {
+ return err
+ }
+
+ // Clean up the subscriber
+ delete(wm.lookup[bucketName][key], id)
+ if len(wm.lookup[bucketName][key]) == 0 {
+ delete(wm.lookup[bucketName], key)
+ }
+ if len(wm.lookup[bucketName]) == 0 {
+ delete(wm.lookup, bucketName)
+ }
+
+ // Close channel if still active
+ if subscriber.active.Load() {
+ close(subscriber.receiveChan)
+ subscriber.active.Store(false)
+ }
+
+ return nil
+}
+
+func (wm *watchManager) cleanUpSubscribers() {
+ wm.mu.Lock()
+ defer wm.mu.Unlock()
+
+ for bucket, bucketMap := range wm.lookup {
+ for key, keyMap := range bucketMap {
+ for _, subscriber := range keyMap {
+ if subscriber.active.Load() {
+ close(subscriber.receiveChan)
+ subscriber.active.Store(false)
+ }
+ delete(keyMap, subscriber.id)
+ }
+ delete(bucketMap, key)
+ }
+ delete(wm.lookup, bucket)
+ }
+}
+
+func (wm *watchManager) close() error {
+ if wm.workerCtx.Err() != nil {
+ return ErrWatchManagerClosed
+ }
+
+ wm.workerCancel()
+
+ wm.closed.Store(true)
+ return nil
+}
+
+func (wm *watchManager) findSubscriber(bucketName BucketName, key string, id uint64) (*subscriber, error) {
+ if _, ok := wm.lookup[bucketName]; !ok {
+ return nil, ErrBucketSubscriberNotFound
+ }
+ if _, ok := wm.lookup[bucketName][key]; !ok {
+ return nil, ErrKeySubscriberNotFound
+ }
+
+ if subscriber, ok := wm.lookup[bucketName][key][id]; ok {
+ return subscriber, nil
+ }
+ return nil, ErrSubscriberNotFound
+}
+
+func (wm *watchManager) done() <-chan struct{} {
+ return wm.workerCtx.Done()
+}
+
+func (wm *watchManager) isClosed() bool {
+ return wm.closed.Load()
+}
+
+/*
+* delete the buckets from the watch manager
+* and notify the subscribers that the keys are deleted due to deleted buckets
+* @param deletedbuckets: the buckets to be deleted
+ */
+func (wm *watchManager) deleteBucket(deletingMessageBucket Message) {
+ bucketName := deletingMessageBucket.BucketName
+
+ if _, ok := wm.lookup[bucketName]; !ok {
+ return
+ }
+
+ identifierId := wm.idGenerator.GenId()
+ victimBucketToSubscribers := make(victimBucketToSubscribers)
+ victimBucketToSubscribers[identifierId] = wm.lookup[bucketName]
+ delete(wm.lookup, bucketName)
+
+ // Log before sending to avoid race condition with victimCollector
+ log.Printf("[watch_manager] Moving bucket %s to victim channel (identifier: %d)\n", bucketName, identifierId)
+
+ // wait for the victim channel to be available
+ timeOut := time.After(10 * time.Second)
+ for {
+ select {
+ case <-timeOut:
+ log.Printf("[watch_manager] Timeout sending victim bucket %s to channel\n", bucketName)
+ return
+ case wm.victimChan <- victimBucketToSubscribers:
+ // Successfully sent - victimCollector now owns the map, don't access it anymore
+ return
+ }
+ }
+}
diff --git a/vendor/github.com/pion/rtp/abscapturetimeextension.go b/vendor/github.com/pion/rtp/abscapturetimeextension.go
index 68b43ca22e..f90f26b621 100644
--- a/vendor/github.com/pion/rtp/abscapturetimeextension.go
+++ b/vendor/github.com/pion/rtp/abscapturetimeextension.go
@@ -5,6 +5,7 @@ package rtp
import (
"encoding/binary"
+ "io"
"time"
)
@@ -30,16 +31,45 @@ type AbsCaptureTimeExtension struct {
EstimatedCaptureClockOffset *int64
}
+// MarshalSize returns the size of the AbsCaptureTimeExtension once marshaled.
+func (t AbsCaptureTimeExtension) MarshalSize() int {
+ if t.EstimatedCaptureClockOffset != nil {
+ return absCaptureTimeExtendedExtensionSize
+ }
+
+ return absCaptureTimeExtensionSize
+}
+
+// MarshalTo marshals the extension to the given buffer.
+// Returns io.ErrShortBuffer if buf is too small.
+func (t AbsCaptureTimeExtension) MarshalTo(buf []byte) (int, error) {
+ if t.EstimatedCaptureClockOffset != nil {
+ if len(buf) < absCaptureTimeExtendedExtensionSize {
+ return 0, io.ErrShortBuffer
+ }
+ binary.BigEndian.PutUint64(buf[0:8], t.Timestamp)
+ binary.BigEndian.PutUint64(buf[8:16], uint64(*t.EstimatedCaptureClockOffset)) // nolint: gosec // G115
+
+ return absCaptureTimeExtendedExtensionSize, nil
+ }
+ if len(buf) < absCaptureTimeExtensionSize {
+ return 0, io.ErrShortBuffer
+ }
+ binary.BigEndian.PutUint64(buf[0:8], t.Timestamp)
+
+ return absCaptureTimeExtensionSize, nil
+}
+
// Marshal serializes the members to buffer.
func (t AbsCaptureTimeExtension) Marshal() ([]byte, error) {
if t.EstimatedCaptureClockOffset != nil {
- buf := make([]byte, 16)
+ buf := make([]byte, absCaptureTimeExtendedExtensionSize)
binary.BigEndian.PutUint64(buf[0:8], t.Timestamp)
binary.BigEndian.PutUint64(buf[8:16], uint64(*t.EstimatedCaptureClockOffset)) // nolint: gosec // G115
return buf, nil
}
- buf := make([]byte, 8)
+ buf := make([]byte, absCaptureTimeExtensionSize)
binary.BigEndian.PutUint64(buf[0:8], t.Timestamp)
return buf, nil
diff --git a/vendor/github.com/pion/rtp/abssendtimeextension.go b/vendor/github.com/pion/rtp/abssendtimeextension.go
index d66e47c966..41c3d47eb2 100644
--- a/vendor/github.com/pion/rtp/abssendtimeextension.go
+++ b/vendor/github.com/pion/rtp/abssendtimeextension.go
@@ -4,6 +4,7 @@
package rtp
import (
+ "io"
"time"
)
@@ -17,6 +18,24 @@ type AbsSendTimeExtension struct {
Timestamp uint64
}
+// MarshalSize returns the size of the AbsSendTimeExtension once marshaled.
+func (t AbsSendTimeExtension) MarshalSize() int {
+ return absSendTimeExtensionSize
+}
+
+// MarshalTo marshals the extension to the given buffer.
+// Returns io.ErrShortBuffer if buf is too small.
+func (t AbsSendTimeExtension) MarshalTo(buf []byte) (int, error) {
+ if len(buf) < absSendTimeExtensionSize {
+ return 0, io.ErrShortBuffer
+ }
+ buf[0] = byte(t.Timestamp & 0xFF0000 >> 16)
+ buf[1] = byte(t.Timestamp & 0xFF00 >> 8)
+ buf[2] = byte(t.Timestamp & 0xFF)
+
+ return absSendTimeExtensionSize, nil
+}
+
// Marshal serializes the members to buffer.
func (t AbsSendTimeExtension) Marshal() ([]byte, error) {
return []byte{
diff --git a/vendor/github.com/pion/rtp/audiolevelextension.go b/vendor/github.com/pion/rtp/audiolevelextension.go
index cc2e652918..3d3e61f357 100644
--- a/vendor/github.com/pion/rtp/audiolevelextension.go
+++ b/vendor/github.com/pion/rtp/audiolevelextension.go
@@ -5,6 +5,7 @@ package rtp
import (
"errors"
+ "io"
)
const (
@@ -40,6 +41,29 @@ type AudioLevelExtension struct {
Voice bool
}
+// MarshalSize returns the size of the AudioLevelExtension once marshaled.
+func (a AudioLevelExtension) MarshalSize() int {
+ return audioLevelExtensionSize
+}
+
+// MarshalTo marshals the extension to the given buffer.
+// Returns io.ErrShortBuffer if buf is too small.
+func (a AudioLevelExtension) MarshalTo(buf []byte) (int, error) {
+ if a.Level > 127 {
+ return 0, errAudioLevelOverflow
+ }
+ if len(buf) < audioLevelExtensionSize {
+ return 0, io.ErrShortBuffer
+ }
+ voice := uint8(0x00)
+ if a.Voice {
+ voice = 0x80
+ }
+ buf[0] = voice | a.Level
+
+ return audioLevelExtensionSize, nil
+}
+
// Marshal serializes the members to buffer.
func (a AudioLevelExtension) Marshal() ([]byte, error) {
if a.Level > 127 {
diff --git a/vendor/github.com/pion/rtp/playoutdelayextension.go b/vendor/github.com/pion/rtp/playoutdelayextension.go
index 81d2d5bf04..188fe07be7 100644
--- a/vendor/github.com/pion/rtp/playoutdelayextension.go
+++ b/vendor/github.com/pion/rtp/playoutdelayextension.go
@@ -6,6 +6,7 @@ package rtp
import (
"encoding/binary"
"errors"
+ "io"
)
const (
@@ -27,6 +28,27 @@ type PlayoutDelayExtension struct {
MinDelay, MaxDelay uint16
}
+// MarshalSize returns the size of the PlayoutDelayExtension once marshaled.
+func (p PlayoutDelayExtension) MarshalSize() int {
+ return playoutDelayExtensionSize
+}
+
+// MarshalTo marshals the extension to the given buffer.
+// Returns io.ErrShortBuffer if buf is too small.
+func (p PlayoutDelayExtension) MarshalTo(buf []byte) (int, error) {
+ if p.MinDelay > playoutDelayMaxValue || p.MaxDelay > playoutDelayMaxValue {
+ return 0, errPlayoutDelayInvalidValue
+ }
+ if len(buf) < playoutDelayExtensionSize {
+ return 0, io.ErrShortBuffer
+ }
+ buf[0] = byte(p.MinDelay >> 4)
+ buf[1] = byte(p.MinDelay<<4) | byte(p.MaxDelay>>8)
+ buf[2] = byte(p.MaxDelay)
+
+ return playoutDelayExtensionSize, nil
+}
+
// Marshal serializes the members to buffer.
func (p PlayoutDelayExtension) Marshal() ([]byte, error) {
if p.MinDelay > playoutDelayMaxValue || p.MaxDelay > playoutDelayMaxValue {
diff --git a/vendor/github.com/pion/rtp/transportccextension.go b/vendor/github.com/pion/rtp/transportccextension.go
index add44ea4c2..aa4fb0a713 100644
--- a/vendor/github.com/pion/rtp/transportccextension.go
+++ b/vendor/github.com/pion/rtp/transportccextension.go
@@ -5,6 +5,7 @@ package rtp
import (
"encoding/binary"
+ "io"
)
const (
@@ -26,6 +27,22 @@ type TransportCCExtension struct {
TransportSequence uint16
}
+// MarshalSize returns the size of the TransportCCExtension once marshaled.
+func (t TransportCCExtension) MarshalSize() int {
+ return transportCCExtensionSize
+}
+
+// MarshalTo marshals the extension to the given buffer.
+// Returns io.ErrShortBuffer if buf is too small.
+func (t TransportCCExtension) MarshalTo(buf []byte) (int, error) {
+ if len(buf) < transportCCExtensionSize {
+ return 0, io.ErrShortBuffer
+ }
+ binary.BigEndian.PutUint16(buf[0:2], t.TransportSequence)
+
+ return transportCCExtensionSize, nil
+}
+
// Marshal serializes the members to buffer.
func (t TransportCCExtension) Marshal() ([]byte, error) {
buf := make([]byte, transportCCExtensionSize)
diff --git a/vendor/github.com/pion/rtp/vlaextension.go b/vendor/github.com/pion/rtp/vlaextension.go
index 8863029e08..21fe629835 100644
--- a/vendor/github.com/pion/rtp/vlaextension.go
+++ b/vendor/github.com/pion/rtp/vlaextension.go
@@ -7,6 +7,7 @@ import (
"encoding/binary"
"errors"
"fmt"
+ "io"
"strings"
"github.com/pion/rtp/codecs/av1/obu"
@@ -49,16 +50,22 @@ type VLA struct {
}
type vlaMarshalingContext struct {
- slMBs [4]uint8
- sls [4][4]*SpatialLayer
- commonSLBM uint8
- encodedTargetBitrates [][]byte
- requiredLen int
+ slMBs [4]uint8
+ slIndices [4][4]int // index into ActiveSpatialLayer, -1 if not set
+ commonSLBM uint8
+ requiredLen int
}
-func (v VLA) preprocessForMashaling(ctx *vlaMarshalingContext) error {
- for i := 0; i < len(v.ActiveSpatialLayer); i++ {
- sl := v.ActiveSpatialLayer[i]
+func (v VLA) preprocessForMashaling(ctx *vlaMarshalingContext) error { //nolint:cyclop
+ // Initialize indices to -1 (not set)
+ for i := range ctx.slIndices {
+ for j := range ctx.slIndices[i] {
+ ctx.slIndices[i][j] = -1
+ }
+ }
+
+ for i := range v.ActiveSpatialLayer {
+ sl := &v.ActiveSpatialLayer[i]
if sl.RTPStreamID < 0 || sl.RTPStreamID >= v.RTPStreamCount {
return fmt.Errorf("invalid RTP streamID %d:%w", sl.RTPStreamID, ErrVLAInvalidStreamID)
}
@@ -69,43 +76,40 @@ func (v VLA) preprocessForMashaling(ctx *vlaMarshalingContext) error {
return fmt.Errorf("invalid temporal layer count %d: %w", len(sl.TargetBitrates), ErrVLAInvalidTemporalLayer)
}
ctx.slMBs[sl.RTPStreamID] |= 1 << sl.SpatialID
- if ctx.sls[sl.RTPStreamID][sl.SpatialID] != nil {
+ if ctx.slIndices[sl.RTPStreamID][sl.SpatialID] != -1 {
return fmt.Errorf("duplicate spatial layer: %w", ErrVLADuplicateSpatialID)
}
- ctx.sls[sl.RTPStreamID][sl.SpatialID] = &sl
+ ctx.slIndices[sl.RTPStreamID][sl.SpatialID] = i
}
return nil
}
-func (v VLA) encodeTargetBitrates(ctx *vlaMarshalingContext) {
+func (v VLA) calcTargetBitratesSize(ctx *vlaMarshalingContext) {
for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ {
for spatialID := 0; spatialID < 4; spatialID++ {
- if sl := ctx.sls[rtpStreamID][spatialID]; sl != nil {
- for _, kbps := range sl.TargetBitrates {
- leb128 := obu.WriteToLeb128(uint(kbps)) // nolint: gosec
- ctx.encodedTargetBitrates = append(ctx.encodedTargetBitrates, leb128)
- ctx.requiredLen += len(leb128)
+ if idx := ctx.slIndices[rtpStreamID][spatialID]; idx >= 0 {
+ for _, kbps := range v.ActiveSpatialLayer[idx].TargetBitrates {
+ ctx.requiredLen += leb128Size(uint(kbps)) //nolint:gosec
}
}
}
}
}
-func (v VLA) analyzeVLAForMarshaling() (*vlaMarshalingContext, error) {
+func (v VLA) analyzeVLAForMarshaling(ctx *vlaMarshalingContext) error {
// Validate RTPStreamCount
if v.RTPStreamCount <= 0 || v.RTPStreamCount > 4 {
- return nil, ErrVLAInvalidStreamCount
+ return ErrVLAInvalidStreamCount
}
// Validate RTPStreamID
if v.RTPStreamID < 0 || v.RTPStreamID >= v.RTPStreamCount {
- return nil, ErrVLAInvalidStreamID
+ return ErrVLAInvalidStreamID
}
- ctx := &vlaMarshalingContext{}
err := v.preprocessForMashaling(ctx)
if err != nil {
- return nil, err
+ return err
}
ctx.commonSLBM = commonSLBMValues(ctx.slMBs[:])
@@ -120,35 +124,49 @@ func (v VLA) analyzeVLAForMarshaling() (*vlaMarshalingContext, error) {
// #tl fields
ctx.requiredLen += (len(v.ActiveSpatialLayer)-1)/4 + 1
- v.encodeTargetBitrates(ctx)
+ v.calcTargetBitratesSize(ctx)
if v.HasResolutionAndFramerate {
ctx.requiredLen += len(v.ActiveSpatialLayer) * 5
}
- return ctx, nil
+ return nil
}
-// Marshal encodes VLA into a byte slice.
-func (v VLA) Marshal() ([]byte, error) { // nolint: cyclop
- ctx, err := v.analyzeVLAForMarshaling()
- if err != nil {
- return nil, err
+// MarshalSize returns the size needed to marshal the VLA.
+func (v VLA) MarshalSize() (int, error) {
+ var ctx vlaMarshalingContext
+ if err := v.analyzeVLAForMarshaling(&ctx); err != nil {
+ return 0, err
+ }
+
+ return ctx.requiredLen, nil
+}
+
+// MarshalTo marshals the VLA to the given buffer.
+// Returns io.ErrShortBuffer if buf is too small.
+func (v VLA) MarshalTo(buf []byte) (int, error) { //nolint:cyclop,gocognit
+ var ctx vlaMarshalingContext
+ if err := v.analyzeVLAForMarshaling(&ctx); err != nil {
+ return 0, err
+ }
+
+ if len(buf) < ctx.requiredLen {
+ return 0, io.ErrShortBuffer
}
- payload := make([]byte, ctx.requiredLen)
offset := 0
// RID, NS, sl_bm fields
- payload[offset] = byte(v.RTPStreamID<<6) | byte(v.RTPStreamCount-1)<<4 | ctx.commonSLBM
+ buf[offset] = byte(v.RTPStreamID<<6) | byte(v.RTPStreamCount-1)<<4 | ctx.commonSLBM
if ctx.commonSLBM == 0 {
offset++
for streamID := 0; streamID < v.RTPStreamCount; streamID++ {
if streamID%2 == 0 {
- payload[offset+streamID/2] |= ctx.slMBs[streamID] << 4
+ buf[offset+streamID/2] |= ctx.slMBs[streamID] << 4
} else {
- payload[offset+streamID/2] |= ctx.slMBs[streamID]
+ buf[offset+streamID/2] |= ctx.slMBs[streamID]
}
}
offset += (v.RTPStreamCount - 1) / 2
@@ -159,12 +177,12 @@ func (v VLA) Marshal() ([]byte, error) { // nolint: cyclop
var temporalLayerIndex int
for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ {
for spatialID := 0; spatialID < 4; spatialID++ {
- if sl := ctx.sls[rtpStreamID][spatialID]; sl != nil {
+ if idx := ctx.slIndices[rtpStreamID][spatialID]; idx >= 0 {
if temporalLayerIndex >= 4 {
temporalLayerIndex = 0
offset++
}
- payload[offset] |= byte(len(sl.TargetBitrates)-1) << (2 * (3 - temporalLayerIndex))
+ buf[offset] |= byte(len(v.ActiveSpatialLayer[idx].TargetBitrates)-1) << (2 * (3 - temporalLayerIndex))
temporalLayerIndex++
}
}
@@ -172,23 +190,43 @@ func (v VLA) Marshal() ([]byte, error) { // nolint: cyclop
// Target bitrate fields
offset++
- for _, encodedKbps := range ctx.encodedTargetBitrates {
- encodedSize := len(encodedKbps)
- copy(payload[offset:], encodedKbps)
- offset += encodedSize
+ for rtpStreamID := 0; rtpStreamID < v.RTPStreamCount; rtpStreamID++ {
+ for spatialID := 0; spatialID < 4; spatialID++ {
+ if idx := ctx.slIndices[rtpStreamID][spatialID]; idx >= 0 {
+ for _, kbps := range v.ActiveSpatialLayer[idx].TargetBitrates {
+ offset += writeLeb128To(buf[offset:], uint(kbps)) //nolint:gosec
+ }
+ }
+ }
}
// Resolution & framerate fields
if v.HasResolutionAndFramerate {
for _, sl := range v.ActiveSpatialLayer {
- binary.BigEndian.PutUint16(payload[offset+0:], uint16(sl.Width-1)) // nolint: gosec
- binary.BigEndian.PutUint16(payload[offset+2:], uint16(sl.Height-1)) // nolint: gosec
- payload[offset+4] = byte(sl.Framerate)
+ binary.BigEndian.PutUint16(buf[offset+0:], uint16(sl.Width-1)) //nolint:gosec
+ binary.BigEndian.PutUint16(buf[offset+2:], uint16(sl.Height-1)) //nolint:gosec
+ buf[offset+4] = byte(sl.Framerate)
offset += 5
}
}
- return payload, nil
+ return ctx.requiredLen, nil
+}
+
+// Marshal encodes VLA into a byte slice.
+func (v VLA) Marshal() ([]byte, error) {
+ size, err := v.MarshalSize()
+ if err != nil {
+ return nil, err
+ }
+
+ buf := make([]byte, size)
+ _, err = v.MarshalTo(buf)
+ if err != nil {
+ return nil, err
+ }
+
+ return buf, nil
}
func commonSLBMValues(slMBs []uint8) uint8 {
@@ -371,3 +409,27 @@ func (v VLA) String() string {
return out
}
+
+// leb128Size returns the number of bytes needed to encode a value as LEB128.
+func leb128Size(in uint) int {
+ size := 1
+ for in >>= 7; in != 0; in >>= 7 {
+ size++
+ }
+
+ return size
+}
+
+// writeLeb128To writes a LEB128 encoded value to buf and returns bytes written.
+func writeLeb128To(buf []byte, in uint) int {
+ for i := range buf {
+ buf[i] = byte(in & 0x7f)
+ in >>= 7
+ if in == 0 {
+ return i + 1
+ }
+ buf[i] |= 0x80
+ }
+
+ return 0
+}
diff --git a/vendor/github.com/pion/sctp/association.go b/vendor/github.com/pion/sctp/association.go
index e47b3feeca..838ae62b2b 100644
--- a/vendor/github.com/pion/sctp/association.go
+++ b/vendor/github.com/pion/sctp/association.go
@@ -6,6 +6,7 @@ package sctp
import (
"bytes"
"context"
+ "encoding/binary"
"errors"
"fmt"
"io"
@@ -117,6 +118,24 @@ const (
maxTSNOffset = 40000
// maxReconfigRequests is the maximum number of reconfig requests we will keep outstanding.
maxReconfigRequests = 1000
+
+ // TLR Adaptive burst mitigation uses quarter-MTU units.
+ // 1 MTU == 4 units, 0.25 MTU == 1 unit.
+ tlrUnitsPerMTU = 4
+
+ // Default burst limits.
+ tlrBurstDefaultFirstRTT = 16 // 4.0 MTU
+ tlrBurstDefaultLaterRTT = 8 // 2.0 MTU
+
+ // Minimum burst limits.
+ tlrBurstMinFirstRTT = 8 // 2.0 MTU
+ tlrBurstMinLaterRTT = 5 // 1.25 MTU
+
+ // Adaptation steps.
+ tlrBurstStepDownFirstRTT = 4 // reduce by 1.0 MTU
+ tlrBurstStepDownLaterRTT = 1 // reduce by 0.25 MTU
+
+ tlrGoodOpsResetThreshold = 16
)
func getAssociationStateString(assoc uint32) string {
@@ -228,6 +247,28 @@ type Association struct {
tReconfig *rtxTimer
ackTimer *ackTimer
+ // RACK / TLP state
+ rackReoWnd time.Duration // dynamic reordering window
+ rackMinRTT time.Duration // min observed RTT
+ rackMinRTTWnd *windowedMin // the window used to determine minRTT, defaults to 30s
+ rackDeliveredTime time.Time // send time of most recently delivered original chunk
+ rackHighestDeliveredOrigTSN uint32
+ rackReorderingSeen bool // ever observed reordering for this association
+ rackKeepInflatedRecoveries int // keep inflated reoWnd for 16 loss recoveries
+ // RACK xmit-time ordered list
+ rackHead *chunkPayloadData
+ rackTail *chunkPayloadData
+
+ // Unified timer for RACK and PTO driven by a single goroutine.
+ // Deadlines are protected with timerMu.
+ timerMu sync.Mutex
+ timerUpdateCh chan struct{}
+ rackDeadline time.Time
+ ptoDeadline time.Time
+
+ rackWCDelAck time.Duration // 200ms default
+ rackReoWndFloor time.Duration
+
// Chunks stored for retransmission
storedInit *chunkInit
storedCookieEcho *chunkCookieEcho
@@ -260,6 +301,18 @@ type Association struct {
name string
log logging.LeveledLogger
+
+ // Adaptive burst mitigation variables
+ tlrActive bool
+ tlrFirstRTT bool // first RTT of this TLR operation
+ tlrHadAdditionalLoss bool
+ tlrEndTSN uint32 // recovery is done when cumAck >= tlrEndTSN
+
+ tlrBurstFirstRTTUnits int64 // quarter-MTU units
+ tlrBurstLaterRTTUnits int64 // quarter-MTU units
+
+ tlrGoodOps uint32 // count of TLR ops completed w/o additional loss
+ tlrStartTime time.Time // time of first recovery RTT
}
// Config collects the arguments to createAssociation construction into
@@ -283,6 +336,15 @@ type Config struct {
FastRtxWnd uint32
// Step of congestion window increase at Congestion Avoidance
CwndCAStep uint32
+
+ // The RACK configs are currently private as SCTP will be reworked to use the
+ // modern options pattern in a future release.
+ // Optional: size of window used to determine minimum RTT for RACK (defaults to 30s)
+ rackMinRTTWnd time.Duration
+ // Optional: cap the minimum reordering window: 0 = use quarter-RTT
+ rackReoWndFloor time.Duration
+ // Optional: receiver worst-case delayed-ACK for PTO when only one packet is in flight
+ rackWCDelAck time.Duration
}
// Server accepts a SCTP stream over a conn.
@@ -392,6 +454,25 @@ func createAssociation(config Config) *Association {
writeNotify: make(chan struct{}, 1),
}
+ // adaptive burst mitigation defaults
+ assoc.tlrBurstFirstRTTUnits = tlrBurstDefaultFirstRTT
+ assoc.tlrBurstLaterRTTUnits = tlrBurstDefaultLaterRTT
+
+ // RACK defaults
+ assoc.rackWCDelAck = config.rackWCDelAck
+ if assoc.rackWCDelAck == 0 {
+ assoc.rackWCDelAck = 200 * time.Millisecond // WCDelAckT, RACK for SCTP section 2C
+ }
+
+ // defaults to 30s window to determine minRTT
+ assoc.rackMinRTTWnd = newWindowedMin(config.rackMinRTTWnd)
+
+ assoc.timerUpdateCh = make(chan struct{}, 1)
+ go assoc.timerLoop()
+
+ assoc.rackReoWndFloor = config.rackReoWndFloor // optional floor; usually 0
+ assoc.rackKeepInflatedRecoveries = 0
+
if assoc.name == "" {
assoc.name = fmt.Sprintf("%p", assoc)
}
@@ -598,6 +679,8 @@ func (a *Association) closeAllTimers() {
a.t3RTX.close()
a.tReconfig.close()
a.ackTimer.close()
+ a.stopRackTimer()
+ a.stopPTOTimer()
}
func (a *Association) readLoop() {
@@ -777,8 +860,8 @@ func (a *Association) handleInbound(raw []byte) error {
}
// The caller should hold the lock.
-func (a *Association) gatherDataPacketsToRetransmit(rawPackets [][]byte) [][]byte {
- for _, p := range a.getDataPacketsToRetransmit() {
+func (a *Association) gatherDataPacketsToRetransmit(rawPackets [][]byte, budgetUnits *int64, consumed *bool) [][]byte {
+ for _, p := range a.getDataPacketsToRetransmit(budgetUnits, consumed) {
raw, err := a.marshalPacket(p)
if err != nil {
a.log.Warnf("[%s] failed to serialize a DATA packet to be retransmitted", a.name)
@@ -794,10 +877,15 @@ func (a *Association) gatherDataPacketsToRetransmit(rawPackets [][]byte) [][]byt
// The caller should hold the lock.
//
//nolint:cyclop
-func (a *Association) gatherOutboundDataAndReconfigPackets(rawPackets [][]byte) [][]byte {
+func (a *Association) gatherOutboundDataAndReconfigPackets(
+ rawPackets [][]byte,
+ budgetUnits *int64,
+ consumed *bool,
+) [][]byte {
// Pop unsent data chunks from the pending queue to send as much as
// cwnd and rwnd allow.
- chunks, sisToReset := a.popPendingDataChunksToSend()
+ chunks, sisToReset := a.popPendingDataChunksToSend(budgetUnits, consumed)
+
if len(chunks) > 0 {
// Start timer. (noop if already started)
a.log.Tracef("[%s] T3-rtx timer start (pt1)", a.name)
@@ -811,6 +899,8 @@ func (a *Association) gatherOutboundDataAndReconfigPackets(rawPackets [][]byte)
}
rawPackets = append(rawPackets, raw)
}
+ // RFC 8985 (RACK) schedule PTO on new data transmission
+ a.schedulePTOAfterSendLocked()
}
if len(sisToReset) > 0 || a.willRetransmitReconfig { //nolint:nestif
@@ -861,63 +951,112 @@ func (a *Association) gatherOutboundDataAndReconfigPackets(rawPackets [][]byte)
// The caller should hold the lock.
//
//nolint:cyclop
-func (a *Association) gatherOutboundFastRetransmissionPackets(rawPackets [][]byte) [][]byte {
- if a.willRetransmitFast { //nolint:nestif
- a.willRetransmitFast = false
+func (a *Association) gatherOutboundFastRetransmissionPackets( //nolint:gocognit
+ rawPackets [][]byte,
+ budgetScaled *int64,
+ consumed *bool,
+) [][]byte {
+ if !a.willRetransmitFast {
+ return rawPackets
+ }
+ a.willRetransmitFast = false
+
+ toFastRetrans := []*chunkPayloadData{}
+ fastRetransSize := int(commonHeaderSize)
+ fastRetransWnd := int(max(a.MTU(), a.fastRtxWnd))
+ now := time.Now()
+
+ // MTU bundling + burst budgeting tracker
+ bytesInPacket := 0
+ stopBundling := false
+
+ for i := 0; ; i++ {
+ chunkPayload, ok := a.inflightQueue.get(a.cumulativeTSNAckPoint + uint32(i) + 1) //nolint:gosec // G115
+ if !ok {
+ break // end of pending data
+ }
- toFastRetrans := []*chunkPayloadData{}
- fastRetransSize := commonHeaderSize
+ if chunkPayload.acked || chunkPayload.abandoned() {
+ continue
+ }
- fastRetransWnd := max(a.MTU(), a.fastRtxWnd)
- for i := 0; ; i++ {
- chunkPayload, ok := a.inflightQueue.get(a.cumulativeTSNAckPoint + uint32(i) + 1) //nolint:gosec // G115
- if !ok {
- break // end of pending data
- }
+ if chunkPayload.nSent > 1 || chunkPayload.missIndicator < 3 {
+ continue
+ }
- if chunkPayload.acked || chunkPayload.abandoned() {
- continue
- }
+ // include padding for sizing.
+ chunkBytes := int(dataChunkHeaderSize) + len(chunkPayload.userData)
+ chunkBytes += getPadding(chunkBytes)
+
+ // fast retransmit window cap
+ if fastRetransWnd < fastRetransSize+chunkBytes {
+ break
+ }
+
+ // MTU bundling + burst budget before mutating
+ for {
+ addBytes := chunkBytes
+
+ if bytesInPacket == 0 {
+ addBytes += int(commonHeaderSize)
+ if addBytes > int(a.MTU()) {
+ stopBundling = true
+
+ break
+ }
+ } else if bytesInPacket+chunkBytes > int(a.MTU()) {
+ // start a new packet and retry this same chunk as first in packet
+ bytesInPacket = 0
- if chunkPayload.nSent > 1 || chunkPayload.missIndicator < 3 {
continue
}
- // RFC 4960 Sec 7.2.4 Fast Retransmit on Gap Reports
- // 3) Determine how many of the earliest (i.e., lowest TSN) DATA chunks
- // marked for retransmission will fit into a single packet, subject
- // to constraint of the path MTU of the destination transport
- // address to which the packet is being sent. Call this value K.
- // Retransmit those K DATA chunks in a single packet. When a Fast
- // Retransmit is being performed, the sender SHOULD ignore the value
- // of cwnd and SHOULD NOT delay retransmission for this single
- // packet.
-
- dataChunkSize := dataChunkHeaderSize + uint32(len(chunkPayload.userData)) //nolint:gosec // G115
- if fastRetransWnd < fastRetransSize+dataChunkSize {
+ if !a.tlrAllowSendLocked(budgetScaled, consumed, addBytes) {
+ // budget exhausted, stop selecting any more fast-rtx chunks
+ stopBundling = true
+
break
}
- fastRetransSize += dataChunkSize
- a.stats.incFastRetrans()
- chunkPayload.nSent++
- a.checkPartialReliabilityStatus(chunkPayload)
- toFastRetrans = append(toFastRetrans, chunkPayload)
- a.log.Tracef("[%s] fast-retransmit: tsn=%d sent=%d htna=%d",
- a.name, chunkPayload.tsn, chunkPayload.nSent, a.fastRecoverExitPoint)
+ if bytesInPacket == 0 {
+ bytesInPacket = int(commonHeaderSize)
+ }
+ bytesInPacket += chunkBytes
+
+ break
}
- if len(toFastRetrans) > 0 {
- for _, p := range a.bundleDataChunksIntoPackets(toFastRetrans) {
- raw, err := a.marshalPacket(p)
- if err != nil {
- a.log.Warnf("[%s] failed to serialize a DATA packet to be fast-retransmitted", a.name)
+ if stopBundling {
+ break
+ }
- continue
- }
- rawPackets = append(rawPackets, raw)
- }
+ fastRetransSize += chunkBytes
+ a.stats.incFastRetrans()
+
+ // Update for retransmission
+ chunkPayload.nSent++
+ chunkPayload.since = now
+ a.rackRemove(chunkPayload)
+ a.rackInsert(chunkPayload)
+
+ a.checkPartialReliabilityStatus(chunkPayload)
+ toFastRetrans = append(toFastRetrans, chunkPayload)
+ a.log.Tracef("[%s] fast-retransmit: tsn=%d sent=%d htna=%d",
+ a.name, chunkPayload.tsn, chunkPayload.nSent, a.fastRecoverExitPoint)
+ }
+
+ if len(toFastRetrans) == 0 {
+ return rawPackets
+ }
+
+ for _, p := range a.bundleDataChunksIntoPackets(toFastRetrans) {
+ raw, err := a.marshalPacket(p)
+ if err != nil {
+ a.log.Warnf("[%s] failed to serialize a DATA packet to be fast-retransmitted", a.name)
+
+ continue
}
+ rawPackets = append(rawPackets, raw)
}
return rawPackets
@@ -1060,14 +1199,23 @@ func (a *Association) gatherOutbound() ([][]byte, bool) {
switch state {
case established:
- rawPackets = a.gatherDataPacketsToRetransmit(rawPackets)
- rawPackets = a.gatherOutboundDataAndReconfigPackets(rawPackets)
- rawPackets = a.gatherOutboundFastRetransmissionPackets(rawPackets)
+ budgetUnits := a.tlrCurrentBurstBudgetScaledLocked()
+ consumed := false
+
+ rawPackets = a.gatherDataPacketsToRetransmit(rawPackets, &budgetUnits, &consumed)
+ rawPackets = a.gatherOutboundDataAndReconfigPackets(rawPackets, &budgetUnits, &consumed)
+ rawPackets = a.gatherOutboundFastRetransmissionPackets(rawPackets, &budgetUnits, &consumed)
+
+ // control traffic shouldn't be limited.
rawPackets = a.gatherOutboundSackPackets(rawPackets)
rawPackets = a.gatherOutboundForwardTSNPackets(rawPackets)
case shutdownPending, shutdownSent, shutdownReceived:
- rawPackets = a.gatherDataPacketsToRetransmit(rawPackets)
- rawPackets = a.gatherOutboundFastRetransmissionPackets(rawPackets)
+ budgetUnits := a.tlrCurrentBurstBudgetScaledLocked()
+ consumed := false
+
+ rawPackets = a.gatherDataPacketsToRetransmit(rawPackets, &budgetUnits, &consumed)
+ rawPackets = a.gatherOutboundFastRetransmissionPackets(rawPackets, &budgetUnits, &consumed)
+
rawPackets = a.gatherOutboundSackPackets(rawPackets)
rawPackets, ok = a.gatherOutboundShutdownPackets(rawPackets)
case shutdownAckSent:
@@ -1406,9 +1554,18 @@ func (a *Association) handleInitAck(pkt *packet, initChunkAck *chunkInitAck) err
// The caller should hold the lock.
func (a *Association) handleHeartbeat(c *chunkHeartbeat) []*packet {
a.log.Tracef("[%s] chunkHeartbeat", a.name)
- hbi, ok := c.params[0].(*paramHeartbeatInfo)
+
+ if len(c.params) == 0 {
+ a.log.Warnf("[%s] Heartbeat without ParamHeartbeatInfo (no params)", a.name)
+
+ return nil
+ }
+
+ info, ok := c.params[0].(*paramHeartbeatInfo)
if !ok {
- a.log.Warnf("[%s] failed to handle Heartbeat, no ParamHeartbeatInfo", a.name)
+ a.log.Warnf("[%s] Heartbeat without ParamHeartbeatInfo (got %T)", a.name, c.params[0])
+
+ return nil
}
return pack(&packet{
@@ -1418,13 +1575,56 @@ func (a *Association) handleHeartbeat(c *chunkHeartbeat) []*packet {
chunks: []chunk{&chunkHeartbeatAck{
params: []param{
¶mHeartbeatInfo{
- heartbeatInformation: hbi.heartbeatInformation,
+ heartbeatInformation: info.heartbeatInformation,
},
},
}},
})
}
+// The caller should hold the lock.
+func (a *Association) handleHeartbeatAck(c *chunkHeartbeatAck) {
+ a.log.Tracef("[%s] chunkHeartbeatAck", a.name)
+
+ if len(c.params) == 0 {
+ return
+ }
+
+ info, ok := c.params[0].(*paramHeartbeatInfo)
+ if !ok {
+ a.log.Warnf("[%s] HeartbeatAck without ParamHeartbeatInfo", a.name)
+
+ return
+ }
+
+ // active RTT probe: if heartbeatInformation is exactly 8 bytes, treat it
+ // as a big-endian unix nano timestamp.
+ if len(info.heartbeatInformation) == 8 {
+ ns := binary.BigEndian.Uint64(info.heartbeatInformation)
+ if ns > math.MaxInt64 {
+ // Malformed or future-unsafe value; ignore this heartbeat-ack.
+ a.log.Warnf("[%s] HB RTT: timestamp overflows int64, ignoring", a.name)
+
+ return
+ }
+
+ sentNanos := int64(ns)
+ sent := time.Unix(0, sentNanos)
+ now := time.Now()
+
+ if !sent.IsZero() && !now.Before(sent) {
+ rttMs := now.Sub(sent).Seconds() * 1000.0
+ srtt := a.rtoMgr.setNewRTT(rttMs)
+ a.srtt.Store(srtt)
+
+ a.rackMinRTTWnd.Push(now, now.Sub(sent))
+
+ a.log.Tracef("[%s] HB RTT: measured=%.3fms srtt=%.3fms rto=%.3fms",
+ a.name, rttMs, srtt, a.rtoMgr.getRTO())
+ }
+ }
+}
+
// The caller should hold the lock.
func (a *Association) handleCookieEcho(cookieEcho *chunkCookieEcho) []*packet {
state := a.getState()
@@ -1530,7 +1730,18 @@ func (a *Association) handleData(chunkPayload *chunkPayloadData) []*packet {
}
}
- return a.handlePeerLastTSNAndAcknowledgement(chunkPayload.immediateSack)
+ // Upon the reception of a new DATA chunk, an endpoint shall examine the
+ // continuity of the TSNs received. If the endpoint detects a gap in
+ // the received DATA chunk sequence, it SHOULD send a SACK with Gap Ack
+ // Blocks immediately. The data receiver continues sending a SACK after
+ // receipt of each SCTP packet that doesn't fill the gap.
+ // https://datatracker.ietf.org/doc/html/rfc4960#section-6.7
+ expectedTSN := a.peerLastTSN() + 1
+ gapDetected := sna32GT(chunkPayload.tsn, expectedTSN)
+
+ sackNow := chunkPayload.immediateSack || gapDetected
+
+ return a.handlePeerLastTSNAndAcknowledgement(sackNow)
}
// A common routine for handleData and handleForwardTSN routines
@@ -1564,17 +1775,25 @@ func (a *Association) handlePeerLastTSNAndAcknowledgement(sackImmediately bool)
a.log.Tracef("[%s] packetloss: %s", a.name, a.payloadQueue.getGapAckBlocksString())
}
- if (a.ackState != ackStateImmediate && !sackImmediately && !hasPacketLoss && a.ackMode == ackModeNormal) ||
- a.ackMode == ackModeAlwaysDelay {
+ // RFC 4960 $6.7: SHOULD ack immediately when detecting a gap.
+ if sackImmediately || hasPacketLoss || a.ackMode == ackModeNoDelay {
+ a.immediateAckTriggered = true
+
+ return reply
+ }
+
+ if a.ackMode == ackModeAlwaysDelay || (a.ackMode == ackModeNormal && a.ackState != ackStateImmediate) {
if a.ackState == ackStateIdle {
a.delayedAckTriggered = true
} else {
a.immediateAckTriggered = true
}
- } else {
- a.immediateAckTriggered = true
+
+ return reply
}
+ a.immediateAckTriggered = true
+
return reply
}
@@ -1673,25 +1892,36 @@ func (a *Association) getOrCreateStream(
// The caller should hold the lock.
//
//nolint:gocognit,cyclop
-func (a *Association) processSelectiveAck(selectiveAckChunk *chunkSelectiveAck) (map[uint16]int, uint32, error) {
- bytesAckedPerStream := map[uint16]int{}
+func (a *Association) processSelectiveAck(selectiveAckChunk *chunkSelectiveAck) (
+ bytesAckedPerStream map[uint16]int,
+ htna uint32,
+ newestDeliveredSendTime time.Time,
+ newestDeliveredOrigTSN uint32,
+ deliveredFound bool,
+ err error,
+) {
+ bytesAckedPerStream = map[uint16]int{}
+ now := time.Now() // capture the time for this SACK
// New ack point, so pop all ACKed packets from inflightQueue
// We add 1 because the "currentAckPoint" has already been popped from the inflight queue
// For the first SACK we take care of this by setting the ackpoint to cumAck - 1
- for i := a.cumulativeTSNAckPoint + 1; sna32LTE(i, selectiveAckChunk.cumulativeTSNAck); i++ {
- chunkPayload, ok := a.inflightQueue.pop(i)
+ for idx := a.cumulativeTSNAckPoint + 1; sna32LTE(idx, selectiveAckChunk.cumulativeTSNAck); idx++ {
+ chunkPayload, ok := a.inflightQueue.pop(idx)
if !ok {
- return nil, 0, fmt.Errorf("%w: %v", ErrInflightQueueTSNPop, i)
+ return nil, 0, time.Time{}, 0, false, fmt.Errorf("%w: %v", ErrInflightQueueTSNPop, idx)
}
- if !chunkPayload.acked {
- // RFC 4096 sec 6.3.2. Retransmission Timer Rules
+ // RACK: remove from xmit-time list since it's delivered
+ a.rackRemove(chunkPayload)
+
+ if !chunkPayload.acked { //nolint:nestif
+ // RFC 4960 sec 6.3.2. Retransmission Timer Rules
// R3) Whenever a SACK is received that acknowledges the DATA chunk
// with the earliest outstanding TSN for that address, restart the
// T3-rtx timer for that address with its current RTO (if there is
// still outstanding data on that address).
- if i == a.cumulativeTSNAckPoint+1 {
+ if idx == a.cumulativeTSNAckPoint+1 {
// T3 timer needs to be reset. Stop it for now.
a.t3RTX.stop()
}
@@ -1714,23 +1944,40 @@ func (a *Association) processSelectiveAck(selectiveAckChunk *chunkSelectiveAck)
// packets that were retransmitted (and thus for which it is
// ambiguous whether the reply was for the first instance of the
// chunk or for a later instance)
- if chunkPayload.nSent == 1 && sna32GTE(chunkPayload.tsn, a.minTSN2MeasureRTT) {
- a.minTSN2MeasureRTT = a.myNextTSN
- rtt := time.Since(chunkPayload.since).Seconds() * 1000.0
- srtt := a.rtoMgr.setNewRTT(rtt)
- a.srtt.Store(srtt)
- a.log.Tracef("[%s] SACK: measured-rtt=%f srtt=%f new-rto=%f",
- a.name, rtt, srtt, a.rtoMgr.getRTO())
+ if sna32GTE(chunkPayload.tsn, a.minTSN2MeasureRTT) {
+ // Only original transmissions for classic RTT measurement (Karn's rule)
+ if chunkPayload.nSent == 1 {
+ a.minTSN2MeasureRTT = a.myNextTSN
+ rtt := now.Sub(chunkPayload.since).Seconds() * 1000.0
+ srtt := a.rtoMgr.setNewRTT(rtt)
+ a.srtt.Store(srtt)
+
+ // use a window to determine minRtt instead of a global min
+ // as the RTT can fluctuate, which can cause problems if going from a
+ // high RTT to a low RTT.
+ a.rackMinRTTWnd.Push(now, now.Sub(chunkPayload.since))
+
+ a.log.Tracef("[%s] SACK: measured-rtt=%f srtt=%f new-rto=%f",
+ a.name, rtt, srtt, a.rtoMgr.getRTO())
+ }
}
- }
- if a.inFastRecovery && chunkPayload.tsn == a.fastRecoverExitPoint {
- a.log.Debugf("[%s] exit fast-recovery", a.name)
- a.inFastRecovery = false
+ // RFC 8985 (RACK) sec 5.2: RACK.segment is the most recently sent
+ // segment that has been delivered, including retransmissions.
+ if chunkPayload.since.After(newestDeliveredSendTime) {
+ newestDeliveredSendTime = chunkPayload.since
+ newestDeliveredOrigTSN = chunkPayload.tsn
+ deliveredFound = true
+ }
+
+ if a.inFastRecovery && chunkPayload.tsn == a.fastRecoverExitPoint {
+ a.log.Debugf("[%s] exit fast-recovery", a.name)
+ a.inFastRecovery = false
+ }
}
}
- htna := selectiveAckChunk.cumulativeTSNAck
+ htna = selectiveAckChunk.cumulativeTSNAck
// Mark selectively acknowledged chunks as "acked"
for _, g := range selectiveAckChunk.gapAckBlocks {
@@ -1738,10 +1985,13 @@ func (a *Association) processSelectiveAck(selectiveAckChunk *chunkSelectiveAck)
tsn := selectiveAckChunk.cumulativeTSNAck + uint32(i)
chunkPayload, ok := a.inflightQueue.get(tsn)
if !ok {
- return nil, 0, fmt.Errorf("%w: %v", ErrTSNRequestNotExist, tsn)
+ return nil, 0, time.Time{}, 0, false, fmt.Errorf("%w: %v", ErrTSNRequestNotExist, tsn)
}
- if !chunkPayload.acked {
+ // RACK: remove from xmit-time list since it's delivered
+ a.rackRemove(chunkPayload)
+
+ if !chunkPayload.acked { //nolint:nestif
nBytesAcked := a.inflightQueue.markAsAcked(tsn)
// Sum the number of bytes acknowledged per stream
@@ -1753,33 +2003,48 @@ func (a *Association) processSelectiveAck(selectiveAckChunk *chunkSelectiveAck)
a.log.Tracef("[%s] tsn=%d has been sacked", a.name, chunkPayload.tsn)
- if chunkPayload.nSent == 1 {
- a.minTSN2MeasureRTT = a.myNextTSN
- rtt := time.Since(chunkPayload.since).Seconds() * 1000.0
- srtt := a.rtoMgr.setNewRTT(rtt)
- a.srtt.Store(srtt)
- a.log.Tracef("[%s] SACK: measured-rtt=%f srtt=%f new-rto=%f",
- a.name, rtt, srtt, a.rtoMgr.getRTO())
+ // RTT / RTO and RACK updates
+ if sna32GTE(chunkPayload.tsn, a.minTSN2MeasureRTT) {
+ // Only original transmissions for classic RTT measurement
+ if chunkPayload.nSent == 1 {
+ a.minTSN2MeasureRTT = a.myNextTSN
+ rtt := now.Sub(chunkPayload.since).Seconds() * 1000.0
+ srtt := a.rtoMgr.setNewRTT(rtt)
+ a.srtt.Store(srtt)
+
+ a.rackMinRTTWnd.Push(now, now.Sub(chunkPayload.since))
+
+ a.log.Tracef("[%s] SACK: measured-rtt=%f srtt=%f new-rto=%f",
+ a.name, rtt, srtt, a.rtoMgr.getRTO())
+ }
}
- if sna32LT(htna, tsn) {
- htna = tsn
+ if chunkPayload.since.After(newestDeliveredSendTime) {
+ newestDeliveredSendTime = chunkPayload.since
+ newestDeliveredOrigTSN = chunkPayload.tsn
+ deliveredFound = true
}
}
+
+ if sna32LT(htna, tsn) {
+ htna = tsn
+ }
}
}
- return bytesAckedPerStream, htna, nil
+ return bytesAckedPerStream, htna, newestDeliveredSendTime, newestDeliveredOrigTSN, deliveredFound, nil
}
// The caller should hold the lock.
func (a *Association) onCumulativeTSNAckPointAdvanced(totalBytesAcked int) {
- // RFC 4096, sec 6.3.2. Retransmission Timer Rules
+ // RFC 4960, sec 6.3.2. Retransmission Timer Rules
// R2) Whenever all outstanding data sent to an address have been
// acknowledged, turn off the T3-rtx timer of that address.
if a.inflightQueue.size() == 0 {
a.log.Tracef("[%s] SACK: no more packet in-flight (pending=%d)", a.name, a.pendingQueue.size())
a.t3RTX.stop()
+ a.stopPTOTimer()
+ a.stopRackTimer()
} else {
a.log.Tracef("[%s] T3-rtx timer start (pt2)", a.name)
a.t3RTX.start(a.rtoMgr.getRTO())
@@ -1787,7 +2052,7 @@ func (a *Association) onCumulativeTSNAckPointAdvanced(totalBytesAcked int) {
// Update congestion control parameters
if a.CWND() <= a.ssthresh { //nolint:nestif
- // RFC 4096, sec 7.2.1. Slow-Start
+ // RFC 4960, sec 7.2.1. Slow-Start
// o When cwnd is less than or equal to ssthresh, an SCTP endpoint MUST
// use the slow-start algorithm to increase cwnd only if the current
// congestion window is being fully utilized, an incoming SACK
@@ -1809,7 +2074,7 @@ func (a *Association) onCumulativeTSNAckPointAdvanced(totalBytesAcked int) {
a.name, a.CWND(), a.ssthresh, totalBytesAcked, a.inFastRecovery, a.pendingQueue.size())
}
} else {
- // RFC 4096, sec 7.2.2. Congestion Avoidance
+ // RFC 4960, sec 7.2.2. Congestion Avoidance
// o Whenever cwnd is greater than ssthresh, upon each SACK arrival
// that advances the Cumulative TSN Ack Point, increase
// partial_bytes_acked by the total number of bytes of all new chunks
@@ -1835,7 +2100,7 @@ func (a *Association) onCumulativeTSNAckPointAdvanced(totalBytesAcked int) {
// The caller should hold the lock.
//
//nolint:cyclop
-func (a *Association) processFastRetransmission(
+func (a *Association) processFastRetransmission( //nolint:gocognit
cumTSNAckPoint uint32,
gapAckBlocks []gapAckBlock,
htna uint32,
@@ -1873,6 +2138,10 @@ func (a *Association) processFastRetransmission(
if !c.acked && !c.abandoned() && c.missIndicator < 3 {
c.missIndicator++
if c.missIndicator == 3 {
+ if a.tlrActive {
+ a.tlrApplyAdditionalLossLocked(time.Now())
+ }
+
if !a.inFastRecovery {
// 2) If not in Fast Recovery, adjust the ssthresh and cwnd of the
// destination address(es) to which the missing DATA chunks were
@@ -1932,7 +2201,9 @@ func (a *Association) handleSack(selectiveAckChunk *chunkSelectiveAck) error {
}
// Process selective ack
- bytesAckedPerStream, htna, err := a.processSelectiveAck(selectiveAckChunk)
+ bytesAckedPerStream, htna,
+ newestDeliveredSendTime, newestDeliveredOrigTSN,
+ deliveredFound, err := a.processSelectiveAck(selectiveAckChunk)
if err != nil {
return err
}
@@ -2011,6 +2282,13 @@ func (a *Association) handleSack(selectiveAckChunk *chunkSelectiveAck) error {
a.postprocessSack(state, cumTSNAckPointAdvanced)
+ // RACK
+ a.onRackAfterSACK(deliveredFound, newestDeliveredSendTime, newestDeliveredOrigTSN, selectiveAckChunk)
+
+ // adaptive burst mitigation
+ ackProgress := cumTSNAckPointAdvanced || deliveredFound
+ a.tlrMaybeFinishLocked(ackProgress)
+
return nil
}
@@ -2367,16 +2645,14 @@ func (a *Association) movePendingDataChunkToInflightQueue(chunkPayload *chunkPay
a.log.Errorf("[%s] failed to pop from pending queue: %s", a.name, err.Error())
}
- // Mark all fragements are in-flight now
if chunkPayload.endingFragment {
chunkPayload.setAllInflight()
}
- // Assign TSN
+ // Assign TSN and original send time
chunkPayload.tsn = a.generateNextTSN()
-
- chunkPayload.since = time.Now() // use to calculate RTT and also for maxPacketLifeTime
- chunkPayload.nSent = 1 // being sent for the first time
+ chunkPayload.since = time.Now()
+ chunkPayload.nSent = 1
a.checkPartialReliabilityStatus(chunkPayload)
@@ -2393,6 +2669,9 @@ func (a *Association) movePendingDataChunkToInflightQueue(chunkPayload *chunkPay
)
a.inflightQueue.pushNoCheck(chunkPayload)
+
+ // RACK: track outstanding original transmissions by send time.
+ a.rackInsert(chunkPayload)
}
// popPendingDataChunksToSend pops chunks from the pending queues as many as
@@ -2400,9 +2679,15 @@ func (a *Association) movePendingDataChunkToInflightQueue(chunkPayload *chunkPay
// The caller should hold the lock.
//
//nolint:cyclop
-func (a *Association) popPendingDataChunksToSend() ([]*chunkPayloadData, []uint16) {
+func (a *Association) popPendingDataChunksToSend( //nolint:cyclop,gocognit
+ budgetScaled *int64,
+ consumed *bool,
+) ([]*chunkPayloadData, []uint16) {
chunks := []*chunkPayloadData{}
- var sisToReset []uint16 // stream identifieres to reset
+ var sisToReset []uint16 // stream indentifiers to reset
+
+ // track current packet size for MTU bundling so budgeting is accurate.
+ bytesInPacket := 0
if a.pendingQueue.size() > 0 { //nolint:nestif
// RFC 4960 sec 6.1. Transmission of DATA Chunks
@@ -2438,19 +2723,59 @@ func (a *Association) popPendingDataChunksToSend() ([]*chunkPayloadData, []uint1
break // no more rwnd
}
+ // compute current DATA chunk size including padding.
+ chunkBytes := int(dataChunkHeaderSize) + len(chunkPayload.userData)
+ chunkBytes += getPadding(chunkBytes)
+
+ // ensure MTU bundling matches bundleDataChunksIntoPackets().
+ addBytes := chunkBytes
+ if bytesInPacket == 0 {
+ addBytes += int(commonHeaderSize)
+ if addBytes > int(a.MTU()) {
+ break
+ }
+
+ // reserve budget for common header + first chunk.
+ if !a.tlrAllowSendLocked(budgetScaled, consumed, addBytes) {
+ break
+ }
+
+ bytesInPacket = int(commonHeaderSize)
+ } else {
+ // if it doesn't fit, start a new packet and retry same chunk.
+ if bytesInPacket+chunkBytes > int(a.MTU()) {
+ bytesInPacket = 0
+
+ continue
+ }
+
+ // reserve budget for the additional chunk bytes.
+ if !a.tlrAllowSendLocked(budgetScaled, consumed, chunkBytes) {
+ break
+ }
+ }
+
a.setRWND(a.RWND() - dataLen)
a.movePendingDataChunkToInflightQueue(chunkPayload)
chunks = append(chunks, chunkPayload)
+ bytesInPacket += chunkBytes
}
- // the data sender can always have one DATA chunk in flight to the receiver
+ // allow one DATA chunk if nothing is inflight to the receiver.
if len(chunks) == 0 && a.inflightQueue.size() == 0 {
// Send zero window probe
c := a.pendingQueue.peek()
- if c != nil {
- a.movePendingDataChunkToInflightQueue(c)
- chunks = append(chunks, c)
+ if c != nil && len(c.userData) > 0 {
+ // probe is a new packet: common header + chunk bytes.
+ chunkBytes := int(dataChunkHeaderSize) + len(c.userData)
+ chunkBytes += getPadding(chunkBytes)
+ addBytes := int(commonHeaderSize) + chunkBytes
+
+ if addBytes <= int(a.MTU()) && a.tlrAllowSendLocked(budgetScaled, consumed, addBytes) {
+ a.movePendingDataChunkToInflightQueue(c)
+ chunks = append(chunks, c)
+ }
}
}
}
@@ -2553,6 +2878,7 @@ func (a *Association) checkPartialReliabilityStatus(chunkPayload *chunkPayloadDa
if stream.reliabilityType == ReliabilityTypeRexmit {
if chunkPayload.nSent >= stream.reliabilityValue {
chunkPayload.setAbandoned(true)
+ a.rackRemove(chunkPayload)
a.log.Tracef(
"[%s] marked as abandoned: tsn=%d ppi=%d (remix: %d)",
a.name, chunkPayload.tsn, chunkPayload.payloadType, chunkPayload.nSent,
@@ -2562,6 +2888,7 @@ func (a *Association) checkPartialReliabilityStatus(chunkPayload *chunkPayloadDa
elapsed := int64(time.Since(chunkPayload.since).Seconds() * 1000)
if elapsed >= int64(stream.reliabilityValue) {
chunkPayload.setAbandoned(true)
+ a.rackRemove(chunkPayload)
a.log.Tracef(
"[%s] marked as abandoned: tsn=%d ppi=%d (timed: %d)",
a.name, chunkPayload.tsn, chunkPayload.payloadType, elapsed,
@@ -2578,13 +2905,15 @@ func (a *Association) checkPartialReliabilityStatus(chunkPayload *chunkPayloadDa
// getDataPacketsToRetransmit is called when T3-rtx is timed out and retransmit outstanding data chunks
// that are not acked or abandoned yet.
// The caller should hold the lock.
-func (a *Association) getDataPacketsToRetransmit() []*packet {
+func (a *Association) getDataPacketsToRetransmit(budgetScaled *int64, consumed *bool) []*packet { //nolint:cyclop
awnd := min32(a.CWND(), a.RWND())
chunks := []*chunkPayloadData{}
var bytesToSend int
- var done bool
+ currRtxTimestamp := time.Now()
- for i := 0; !done; i++ {
+ bytesInPacket := 0
+
+ for i := 0; ; i++ {
chunkPayload, ok := a.inflightQueue.get(a.cumulativeTSNAckPoint + uint32(i) + 1) //nolint:gosec // G115
if !ok {
break // end of pending data
@@ -2595,18 +2924,49 @@ func (a *Association) getDataPacketsToRetransmit() []*packet {
}
if i == 0 && int(a.RWND()) < len(chunkPayload.userData) {
- // Send it as a zero window probe
- done = true
+ // allow as zero window probe
} else if bytesToSend+len(chunkPayload.userData) > int(awnd) {
break
}
- // reset the retransmit flag not to retransmit again before the next
- // t3-rtx timer fires
+ chunkBytes := int(dataChunkHeaderSize) + len(chunkPayload.userData)
+ chunkBytes += getPadding(chunkBytes)
+
+ // retry as first chunk in a new packet if needed.
+ for {
+ addBytes := chunkBytes
+ if bytesInPacket == 0 {
+ addBytes += int(commonHeaderSize)
+ if addBytes > int(a.MTU()) {
+ return a.bundleDataChunksIntoPackets(chunks)
+ }
+ } else if bytesInPacket+chunkBytes > int(a.MTU()) {
+ bytesInPacket = 0
+
+ continue
+ }
+
+ // burst budget gate before mutating the chunk.
+ if !a.tlrAllowSendLocked(budgetScaled, consumed, addBytes) {
+ return a.bundleDataChunksIntoPackets(chunks)
+ }
+
+ if bytesInPacket == 0 {
+ bytesInPacket = int(commonHeaderSize)
+ }
+ bytesInPacket += chunkBytes
+
+ break
+ }
+
chunkPayload.retransmit = false
bytesToSend += len(chunkPayload.userData)
+ // Update for retransmission
chunkPayload.nSent++
+ chunkPayload.since = currRtxTimestamp
+ a.rackRemove(chunkPayload)
+ a.rackInsert(chunkPayload)
a.checkPartialReliabilityStatus(chunkPayload)
@@ -2716,10 +3076,12 @@ func (a *Association) handleChunk(receivedPacket *packet, receivedChunk chunk) e
}
a.log.Debugf("[%s] Error chunk, with following errors: %s", a.name, errStr)
- // Note: chunkHeartbeatAck not handled?
case *chunkHeartbeat:
packets = a.handleHeartbeat(receivedChunk)
+ case *chunkHeartbeatAck:
+ a.handleHeartbeatAck(receivedChunk)
+
case *chunkCookieEcho:
packets = a.handleCookieEcho(receivedChunk)
@@ -2949,3 +3311,689 @@ func (a *Association) completeHandshake(handshakeErr error) bool {
return false
}
+
+func (a *Association) pokeTimerLoop() {
+ // enqueue a single wake-up without blocking.
+ select {
+ case a.timerUpdateCh <- struct{}{}:
+ default:
+ }
+}
+
+func (a *Association) startRackTimer(dur time.Duration) {
+ a.timerMu.Lock()
+
+ if dur <= 0 {
+ a.rackDeadline = time.Time{}
+ } else {
+ a.rackDeadline = time.Now().Add(dur)
+ }
+
+ a.timerMu.Unlock()
+
+ a.pokeTimerLoop()
+}
+
+func (a *Association) stopRackTimer() {
+ a.timerMu.Lock()
+ a.rackDeadline = time.Time{}
+ a.timerMu.Unlock()
+
+ a.pokeTimerLoop()
+}
+
+func (a *Association) startPTOTimer(dur time.Duration) {
+ a.timerMu.Lock()
+
+ if dur <= 0 {
+ a.ptoDeadline = time.Time{}
+ } else {
+ a.ptoDeadline = time.Now().Add(dur)
+ }
+
+ a.timerMu.Unlock()
+
+ a.pokeTimerLoop()
+}
+
+func (a *Association) stopPTOTimer() {
+ a.timerMu.Lock()
+ a.ptoDeadline = time.Time{}
+ a.timerMu.Unlock()
+
+ a.pokeTimerLoop()
+}
+
+// drainTimer safely stops a timer and drains its channel if needed.
+func drainTimer(t *time.Timer) {
+ if !t.Stop() {
+ select {
+ case <-t.C:
+ default:
+ }
+ }
+}
+
+// timerLoop runs one goroutine per association for RACK and PTO deadlines.
+// this only runs if RACK is enabled.
+func (a *Association) timerLoop() { //nolint:gocognit,cyclop
+ // begin with a disarmed timer.
+ timer := time.NewTimer(time.Hour)
+ drainTimer(timer)
+ armed := false
+
+ for {
+ // compute the earliest non-zero deadline.
+ a.timerMu.Lock()
+ rackDeadline := a.rackDeadline
+ ptoDeadline := a.ptoDeadline
+ a.timerMu.Unlock()
+
+ var next time.Time
+ switch {
+ case rackDeadline.IsZero():
+ next = ptoDeadline
+ case ptoDeadline.IsZero():
+ next = rackDeadline
+ default:
+ if rackDeadline.Before(ptoDeadline) {
+ next = rackDeadline
+ } else {
+ next = ptoDeadline
+ }
+ }
+
+ if next.IsZero() {
+ if armed {
+ drainTimer(timer)
+ armed = false
+ }
+ } else {
+ d := time.Until(next)
+
+ if d <= 0 {
+ d = time.Nanosecond
+ }
+
+ if armed {
+ drainTimer(timer)
+ }
+
+ timer.Reset(d)
+ armed = true
+ }
+
+ select {
+ case <-a.closeWriteLoopCh:
+ if armed {
+ drainTimer(timer)
+ }
+
+ return
+
+ case <-a.timerUpdateCh:
+ // re-compute deadlines and (re)arm in next loop iteration.
+
+ case <-timer.C:
+ armed = false
+
+ // snapshot & clear due deadlines before firing to avoid races with re-arms.
+ currTime := time.Now()
+ var fireRack, firePTO bool
+
+ a.timerMu.Lock()
+
+ if !a.rackDeadline.IsZero() && !currTime.Before(a.rackDeadline) {
+ fireRack = true
+ a.rackDeadline = time.Time{}
+ }
+
+ if !a.ptoDeadline.IsZero() && !currTime.Before(a.ptoDeadline) {
+ firePTO = true
+ a.ptoDeadline = time.Time{}
+ }
+
+ a.timerMu.Unlock()
+
+ // fire callbacks without holding timerMu.
+ if fireRack {
+ a.onRackTimeout()
+ }
+
+ if firePTO {
+ a.onPTOTimer()
+ }
+ }
+ }
+}
+
+// onRackAfterSACK implements the RACK logic (RACK for SCTP section 2A/B, section 3) and TLP scheduling (section 2C).
+func (a *Association) onRackAfterSACK( // nolint:gocognit,cyclop,gocyclo
+ deliveredFound bool,
+ newestDeliveredSendTime time.Time,
+ newestDeliveredOrigTSN uint32,
+ sack *chunkSelectiveAck,
+) {
+ // store the current time for when we check if it's needed in step 2 (whether we should maintain ReoWND)
+ currTime := time.Now()
+
+ // 1) Update highest delivered original TSN for reordering detection (section 2B)
+ if deliveredFound {
+ if sna32LT(a.rackHighestDeliveredOrigTSN, newestDeliveredOrigTSN) {
+ a.rackHighestDeliveredOrigTSN = newestDeliveredOrigTSN
+ } else {
+ // ACK of an original TSN below the high-watermark -> reordering observed
+ a.rackReorderingSeen = true
+ }
+ if newestDeliveredSendTime.After(a.rackDeliveredTime) {
+ a.rackDeliveredTime = newestDeliveredSendTime
+ }
+ }
+
+ // 2) Maintain ReoWND (RACK for SCTP section 2B)
+ if minRTT := a.rackMinRTTWnd.Min(currTime); minRTT > 0 {
+ a.rackMinRTT = minRTT
+ }
+
+ var base time.Duration
+ if a.rackMinRTT > 0 {
+ base = max(a.rackMinRTT/4, a.rackReoWndFloor)
+ }
+
+ // Suppress during recovery if no reordering ever seen; else (re)initialize from base if zero.
+ if !a.rackReorderingSeen && (a.inFastRecovery || a.t3RTX.isRunning()) {
+ a.rackReoWnd = 0
+ } else if a.rackReoWnd == 0 && base > 0 {
+ a.rackReoWnd = base
+ }
+
+ // DSACK-style inflation using SCTP duplicate TSNs (RACK for SCTP section 3 noting SCTP
+ // natively reports duplicates + RACK for SCTP section 2B policy)
+ if len(sack.duplicateTSN) > 0 && a.rackMinRTT > 0 {
+ a.rackReoWnd += max(a.rackMinRTT/4, a.rackReoWndFloor)
+ // keep inflated for 16 loss recoveries before reset
+ a.rackKeepInflatedRecoveries = 16
+ a.log.Tracef("[%s] RACK: DSACK/dupTSN seen, inflate reoWnd to %v", a.name, a.rackReoWnd)
+ }
+
+ // decrement the keep inflated counter when we leave recovery
+ if !a.inFastRecovery && a.rackKeepInflatedRecoveries > 0 {
+ a.rackKeepInflatedRecoveries--
+ if a.rackKeepInflatedRecoveries == 0 && a.rackMinRTT > 0 {
+ a.rackReoWnd = a.rackMinRTT / 4
+ }
+ }
+
+ // RFC 8985: the reordering window MUST be bounded by SRTT.
+ if srttMs := a.SRTT(); srttMs > 0 {
+ if srttDur := time.Duration(srttMs * 1e6); a.rackReoWnd > srttDur {
+ a.rackReoWnd = srttDur
+ }
+ }
+
+ // 3) Loss marking on ACK: any outstanding chunk whose (send_time + reoWnd) < newestDeliveredSendTime
+ // is lost (RACK for SCTP section 2A)
+ if !a.rackDeliveredTime.IsZero() { //nolint:nestif
+ marked := false
+
+ for chunk := a.rackHead; chunk != nil; {
+ next := chunk.rackNext // save in case we remove c
+
+ // but clean up if they exist.
+ if chunk.acked || chunk.abandoned() {
+ a.rackRemove(chunk)
+ chunk = next
+
+ continue
+ }
+
+ if chunk.retransmit || chunk.nSent > 1 {
+ // Either already scheduled for retransmit or not an original send:
+ // skip but keep in list in case it's still outstanding.
+ chunk = next
+
+ continue
+ }
+
+ // Ordered by original send time. If this one is too new,
+ // all later ones are even newer -> short-circuit.
+ if !chunk.since.Add(a.rackReoWnd).Before(a.rackDeliveredTime) {
+ break
+ }
+
+ // Mark as lost by RACK.
+ chunk.retransmit = true
+ marked = true
+
+ // Remove from xmit-time list: we no longer need RACK for this TSN.
+ a.rackRemove(chunk)
+
+ a.log.Tracef("[%s] RACK: mark lost tsn=%d (sent=%v, delivered=%v, reoWnd=%v)",
+ a.name, chunk.tsn, chunk.since, a.rackDeliveredTime, a.rackReoWnd)
+
+ chunk = next
+ }
+
+ if marked {
+ // loss detected during active TLR so we must reduce burst
+ if a.tlrActive {
+ a.tlrApplyAdditionalLossLocked(currTime)
+ }
+
+ a.awakeWriteLoop()
+ }
+ }
+
+ // 4) Arm the RACK timer if there are still outstanding but not-yet-overdue chunks (RACK for SCTP section 2A)
+ if a.rackHead != nil && !a.rackDeliveredTime.IsZero() {
+ // RackRTT = RTT of the most recently delivered packet
+ rackRTT := max(time.Since(a.rackDeliveredTime), time.Duration(0))
+ a.startRackTimer(rackRTT + a.rackReoWnd) // RACK for SCTP section 2A
+ } else {
+ a.stopRackTimer()
+ }
+
+ // 5) Re/schedule Tail Loss Probe (PTO) (RACK for SCTP section 2C)
+ // Triggered when new data is sent or cum-ack advances; we approximate by scheduling on every SACK that advanced
+ if a.inflightQueue.size() == 0 {
+ a.stopPTOTimer()
+
+ return
+ }
+
+ var pto time.Duration
+ srttMs := a.SRTT()
+ if srttMs > 0 {
+ srtt := time.Duration(srttMs * 1e6)
+ extra := 2 * time.Millisecond
+
+ if a.inflightQueue.size() == 1 {
+ extra = a.rackWCDelAck // 200ms for single outstanding, else 2ms
+ }
+
+ pto = 2*srtt + extra
+ } else {
+ pto = time.Second // no RTT yet
+ }
+
+ a.startPTOTimer(pto)
+}
+
+// schedulePTOAfterSendLocked starts/restarts the PTO timer when new data is transmitted.
+// Caller must hold a.lock.
+func (a *Association) schedulePTOAfterSendLocked() {
+ if a.inflightQueue.size() == 0 {
+ a.stopPTOTimer()
+
+ return
+ }
+
+ var pto time.Duration
+ if srttMs := a.SRTT(); srttMs > 0 {
+ srtt := time.Duration(srttMs * 1e6)
+ extra := 2 * time.Millisecond
+
+ if a.inflightQueue.size() == 1 {
+ extra = a.rackWCDelAck
+ }
+
+ pto = 2*srtt + extra
+ } else {
+ pto = time.Second
+ }
+
+ a.startPTOTimer(pto)
+}
+
+// onRackTimeout is fired to avoid waiting for the next ACK.
+func (a *Association) onRackTimeout() {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+ a.onRackTimeoutLocked()
+}
+
+func (a *Association) onRackTimeoutLocked() { //nolint:cyclop
+ if a.rackDeliveredTime.IsZero() {
+ return
+ }
+
+ marked := false
+
+ for chunk := a.rackHead; chunk != nil; {
+ next := chunk.rackNext
+
+ if chunk.acked || chunk.abandoned() {
+ a.rackRemove(chunk)
+ chunk = next
+
+ continue
+ }
+ if chunk.retransmit || chunk.nSent > 1 {
+ chunk = next
+
+ continue
+ }
+
+ if !chunk.since.Add(a.rackReoWnd).Before(a.rackDeliveredTime) {
+ // too new, later ones are newer so we can skip.
+ break
+ }
+
+ chunk.retransmit = true
+ marked = true
+ a.rackRemove(chunk)
+
+ a.log.Tracef("[%s] RACK timer: mark lost tsn=%d", a.name, chunk.tsn)
+
+ chunk = next
+ }
+
+ if marked {
+ // loss detected during active TLR so we must reduce burst
+ if a.tlrActive {
+ a.tlrApplyAdditionalLossLocked(time.Now())
+ }
+
+ a.awakeWriteLoop()
+ }
+}
+
+func (a *Association) onPTOTimer() {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+ a.onPTOTimerLocked()
+}
+
+func (a *Association) onPTOTimerLocked() {
+ // if nothing is inflight, PTO should not drive TLR.
+ // use PTO as a chance to probe RTT via HEARTBEAT instead of retransmitting DATA.
+ if a.inflightQueue.size() == 0 {
+ a.stopPTOTimer()
+ a.log.Tracef("[%s] PTO idle: sending active HEARTBEAT for RTT probe", a.name)
+ a.sendActiveHeartbeatLocked()
+
+ return
+ }
+
+ currTime := time.Now()
+
+ if !a.tlrActive {
+ a.tlrBeginLocked()
+ } else {
+ a.tlrApplyAdditionalLossLocked(currTime)
+ }
+
+ // If we have unsent data, PTO should just wake the writer.
+ if a.pendingQueue.size() > 0 {
+ a.awakeWriteLoop()
+
+ return
+ }
+
+ // otherwise retransmit most recently sent in-flight DATA.
+ var latest *chunkPayloadData
+ for i := uint32(0); ; i++ {
+ c, ok := a.inflightQueue.get(a.cumulativeTSNAckPoint + i + 1)
+ if !ok {
+ break
+ }
+
+ if c.acked || c.abandoned() {
+ continue
+ }
+
+ latest = c
+ }
+
+ if latest != nil && !latest.retransmit {
+ latest.retransmit = true
+ a.log.Tracef("[%s] PTO fired: probe tsn=%d", a.name, latest.tsn)
+ a.awakeWriteLoop()
+ }
+}
+
+func (a *Association) rackInsert(c *chunkPayloadData) {
+ if c == nil || c.rackInList {
+ return
+ }
+
+ if a.rackTail != nil {
+ a.rackTail.rackNext = c
+ c.rackPrev = a.rackTail
+ } else {
+ a.rackHead = c
+ }
+ a.rackTail = c
+ c.rackInList = true
+}
+
+func (a *Association) rackRemove(chunk *chunkPayloadData) {
+ if chunk == nil || !chunk.rackInList {
+ return
+ }
+
+ if prev := chunk.rackPrev; prev != nil {
+ prev.rackNext = chunk.rackNext
+ } else {
+ a.rackHead = chunk.rackNext
+ }
+
+ if next := chunk.rackNext; next != nil {
+ next.rackPrev = chunk.rackPrev
+ } else {
+ a.rackTail = chunk.rackPrev
+ }
+
+ chunk.rackPrev = nil
+ chunk.rackNext = nil
+ chunk.rackInList = false
+}
+
+// caller must hold a.lock.
+func (a *Association) tlrFirstRTTDurationLocked() time.Duration {
+ // Use SRTT when available; fall back to a safe default.
+ if srttMs := a.SRTT(); srttMs > 0 {
+ return time.Duration(srttMs * 1e6)
+ }
+
+ return time.Second
+}
+
+// caller must hold a.lock.
+func (a *Association) tlrUpdatePhaseLocked(currTime time.Time) {
+ if !a.tlrActive || !a.tlrFirstRTT {
+ return
+ }
+ if a.tlrStartTime.IsZero() {
+ return
+ }
+
+ if currTime.Sub(a.tlrStartTime) >= a.tlrFirstRTTDurationLocked() {
+ a.tlrFirstRTT = false
+ }
+}
+
+// caller must hold a.lock.
+func (a *Association) tlrCurrentBurstUnitsLocked() int64 {
+ if !a.tlrActive {
+ return 0
+ }
+
+ a.tlrUpdatePhaseLocked(time.Now())
+
+ if a.tlrFirstRTT {
+ return a.tlrBurstFirstRTTUnits
+ }
+
+ return a.tlrBurstLaterRTTUnits
+}
+
+// caller must hold a.lock.
+// Returns remaining burst budget in "scaled bytes": bytes * 4 (quarter-MTU precision).
+func (a *Association) tlrCurrentBurstBudgetScaledLocked() int64 {
+ if !a.tlrActive {
+ return 0
+ }
+
+ units := a.tlrCurrentBurstUnitsLocked()
+
+ return units * int64(a.MTU())
+}
+
+// caller must hold a.lock.
+func (a *Association) tlrHighestOutstandingTSNLocked() (uint32, bool) {
+ var last uint32
+ found := false
+
+ for i := uint32(0); ; i++ {
+ tsn := a.cumulativeTSNAckPoint + i + 1
+ _, ok := a.inflightQueue.get(tsn)
+ if !ok {
+ break
+ }
+ last = tsn
+ found = true
+ }
+
+ return last, found
+}
+
+// caller must hold a.lock.
+func (a *Association) tlrBeginLocked() {
+ currTime := time.Now()
+
+ a.tlrActive = true
+ a.tlrFirstRTT = true
+ a.tlrHadAdditionalLoss = false
+ a.tlrStartTime = currTime
+
+ if endTSN, ok := a.tlrHighestOutstandingTSNLocked(); ok {
+ a.tlrEndTSN = endTSN
+ } else {
+ a.tlrEndTSN = a.cumulativeTSNAckPoint
+ }
+}
+
+// caller must hold a.lock.
+func (a *Association) tlrApplyAdditionalLossLocked(currTime time.Time) {
+ if !a.tlrActive {
+ return
+ }
+
+ // Decide whether we're still within the first recovery RTT window.
+ a.tlrUpdatePhaseLocked(currTime)
+
+ a.tlrHadAdditionalLoss = true
+ a.tlrGoodOps = 0
+
+ if a.tlrFirstRTT {
+ // Loss during first recovery RTT => initial burst too high.
+ a.tlrBurstFirstRTTUnits -= tlrBurstStepDownFirstRTT
+ if a.tlrBurstFirstRTTUnits < tlrBurstMinFirstRTT {
+ a.tlrBurstFirstRTTUnits = tlrBurstMinFirstRTT
+ }
+ } else {
+ // Loss during later RTTs => increasing rate too high.
+ a.tlrBurstLaterRTTUnits -= tlrBurstStepDownLaterRTT
+ if a.tlrBurstLaterRTTUnits < tlrBurstMinLaterRTT {
+ a.tlrBurstLaterRTTUnits = tlrBurstMinLaterRTT
+ }
+ }
+}
+
+// caller must hold a.lock.
+func (a *Association) tlrMaybeFinishLocked(ackProgress bool) {
+ if !a.tlrActive {
+ return
+ }
+
+ // determine if we should move from the first RTT burst to later RTT burst.
+ if a.tlrFirstRTT && ackProgress {
+ a.tlrFirstRTT = false
+ }
+
+ // finish once cumulatively ACKed through the tail we were recovering.
+ if sna32GTE(a.cumulativeTSNAckPoint, a.tlrEndTSN) {
+ if !a.tlrHadAdditionalLoss {
+ a.tlrGoodOps++
+ if a.tlrGoodOps >= tlrGoodOpsResetThreshold {
+ a.tlrBurstFirstRTTUnits = tlrBurstDefaultFirstRTT
+ a.tlrBurstLaterRTTUnits = tlrBurstDefaultLaterRTT
+ a.tlrGoodOps = 0
+ }
+ } else {
+ a.tlrGoodOps = 0
+ }
+
+ a.tlrActive = false
+ a.tlrFirstRTT = false
+ a.tlrHadAdditionalLoss = false
+ a.tlrEndTSN = 0
+ }
+}
+
+// caller must hold a.lock.
+// "budgetScaled" is remaining burst budget in (bytes*4) scale.
+// "consumed" allows the first send in a burst.
+func (a *Association) tlrAllowSendLocked(budgetScaled *int64, consumed *bool, estBytes int) bool {
+ if !a.tlrActive || budgetScaled == nil || consumed == nil {
+ return true
+ }
+ if estBytes <= 0 {
+ return true
+ }
+
+ needScaled := int64(estBytes) * tlrUnitsPerMTU // bytes*4
+ if *consumed && *budgetScaled < needScaled {
+ return false
+ }
+
+ *budgetScaled -= needScaled
+ if *budgetScaled < 0 {
+ *budgetScaled = 0
+ }
+ *consumed = true
+
+ return true
+}
+
+// ActiveHeartbeat sends a HEARTBEAT chunk on the association to perform an
+// on-demand RTT measurement without application payload.
+//
+// It is safe to call from outside; it will take the association lock and
+// be a no-op if the association is not established.
+func (a *Association) ActiveHeartbeat() {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ if a.getState() != established {
+ return
+ }
+
+ a.sendActiveHeartbeatLocked()
+}
+
+// caller must hold a.lock.
+func (a *Association) sendActiveHeartbeatLocked() {
+ now := time.Now().UnixNano()
+ buf := make([]byte, 8)
+ binary.BigEndian.PutUint64(buf, uint64(now)) //nolint:gosec // time.now() will never be negative
+
+ info := ¶mHeartbeatInfo{heartbeatInformation: buf}
+
+ hb := &chunkHeartbeat{
+ chunkHeader: chunkHeader{
+ typ: ctHeartbeat,
+ flags: 0,
+ },
+ params: []param{info},
+ }
+
+ a.controlQueue.push(&packet{
+ verificationTag: a.peerVerificationTag,
+ sourcePort: a.sourcePort,
+ destinationPort: a.destinationPort,
+ chunks: []chunk{hb},
+ })
+ a.awakeWriteLoop()
+}
diff --git a/vendor/github.com/pion/sctp/chunk_heartbeat.go b/vendor/github.com/pion/sctp/chunk_heartbeat.go
index 341e5442c6..74d24bfa85 100644
--- a/vendor/github.com/pion/sctp/chunk_heartbeat.go
+++ b/vendor/github.com/pion/sctp/chunk_heartbeat.go
@@ -9,32 +9,16 @@ import (
)
/*
-chunkHeartbeat represents an SCTP Chunk of type HEARTBEAT
+chunkHeartbeat represents an SCTP Chunk of type HEARTBEAT (RFC 9260 section 3.3.6)
-An endpoint should send this chunk to its peer endpoint to probe the
-reachability of a particular destination transport address defined in
-the present association.
+An endpoint sends this chunk to probe reachability of a destination address.
+The chunk MUST contain exactly one variable-length parameter:
-The parameter field contains the Heartbeat Information, which is a
-variable-length opaque data structure understood only by the sender.
-
- 0 1 2 3
- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | Type = 4 | Chunk Flags | Heartbeat Length |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- | |
- | Heartbeat Information TLV (Variable-Length) |
- | |
- +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-
-Defined as a variable-length parameter using the format described
-in Section 3.2.1, i.e.:
-
-Variable Parameters Status Type Value
+Variable Parameters Status Type Value
-------------------------------------------------------------
-heartbeat Info Mandatory 1
-.
+Heartbeat Info Mandatory 1
+
+nolint:godot
*/
type chunkHeartbeat struct {
chunkHeader
@@ -48,20 +32,30 @@ var (
ErrParseParamTypeFailed = errors.New("failed to parse param type")
ErrHeartbeatParam = errors.New("heartbeat should only have HEARTBEAT param")
ErrHeartbeatChunkUnmarshal = errors.New("failed unmarshalling param in Heartbeat Chunk")
+ ErrHeartbeatExtraNonZero = errors.New("heartbeat has non-zero trailing bytes after last parameter")
+ ErrHeartbeatMarshalNoInfo = errors.New("heartbeat marshal requires exactly one Heartbeat Info parameter")
)
-func (h *chunkHeartbeat) unmarshal(raw []byte) error {
+func (h *chunkHeartbeat) unmarshal(raw []byte) error { //nolint:cyclop
if err := h.chunkHeader.unmarshal(raw); err != nil {
return err
- } else if h.typ != ctHeartbeat {
+ }
+
+ if h.typ != ctHeartbeat {
return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotHeartbeat, h.typ.String())
}
- if len(raw) <= chunkHeaderSize {
- return fmt.Errorf("%w: %d", ErrHeartbeatNotLongEnoughInfo, len(raw))
+ // if the body is completely empty, accept it but don't populate params.
+ if len(h.raw) == 0 {
+ return nil
}
- pType, err := parseParamType(raw[chunkHeaderSize:])
+ // need at least a parameter header present (TLV: 4 bytes minimum).
+ if len(h.raw) < initOptionalVarHeaderLength {
+ return fmt.Errorf("%w: %d", ErrHeartbeatNotLongEnoughInfo, len(h.raw))
+ }
+
+ pType, err := parseParamType(h.raw)
if err != nil {
return fmt.Errorf("%w: %v", ErrParseParamTypeFailed, err) //nolint:errorlint
}
@@ -69,17 +63,52 @@ func (h *chunkHeartbeat) unmarshal(raw []byte) error {
return fmt.Errorf("%w: instead have %s", ErrHeartbeatParam, pType.String())
}
- p, err := buildParam(pType, raw[chunkHeaderSize:])
+ var pHeader paramHeader
+ if e := pHeader.unmarshal(h.raw); e != nil {
+ return fmt.Errorf("%w: %v", ErrParseParamTypeFailed, e) //nolint:errorlint
+ }
+
+ plen := pHeader.length()
+ if plen < initOptionalVarHeaderLength || plen > len(h.raw) {
+ return ErrHeartbeatNotLongEnoughInfo
+ }
+
+ p, err := buildParam(pType, h.raw[:plen])
if err != nil {
return fmt.Errorf("%w: %v", ErrHeartbeatChunkUnmarshal, err) //nolint:errorlint
}
h.params = append(h.params, p)
+ // any trailing bytes beyond the single param must be all zeros.
+ if rem := h.raw[plen:]; len(rem) > 0 && !allZero(rem) {
+ return ErrHeartbeatExtraNonZero
+ }
+
return nil
}
func (h *chunkHeartbeat) Marshal() ([]byte, error) {
- return nil, ErrUnimplemented
+ // exactly one Heartbeat Info param is required.
+ if len(h.params) != 1 {
+ return nil, ErrHeartbeatMarshalNoInfo
+ }
+
+ // enforce correct concrete type via type assertion (param interface has no type getter).
+ if _, ok := h.params[0].(*paramHeartbeatInfo); !ok {
+ return nil, ErrHeartbeatParam
+ }
+
+ pp, err := h.params[0].marshal()
+ if err != nil {
+ return nil, fmt.Errorf("%w: %v", ErrHeartbeatChunkUnmarshal, err) //nolint:errorlint
+ }
+
+ // single TLV, no inter-parameter padding within the chunk body.
+ h.chunkHeader.typ = ctHeartbeat
+ h.chunkHeader.flags = 0 // sender MUST set to 0
+ h.chunkHeader.raw = append([]byte(nil), pp...)
+
+ return h.chunkHeader.marshal()
}
func (h *chunkHeartbeat) check() (abort bool, err error) {
diff --git a/vendor/github.com/pion/sctp/chunk_heartbeat_ack.go b/vendor/github.com/pion/sctp/chunk_heartbeat_ack.go
index 8ccc153125..60e14af0a0 100644
--- a/vendor/github.com/pion/sctp/chunk_heartbeat_ack.go
+++ b/vendor/github.com/pion/sctp/chunk_heartbeat_ack.go
@@ -43,14 +43,63 @@ type chunkHeartbeatAck struct {
// Heartbeat ack chunk errors.
var (
+ // Deprecated: this error is no longer used but is kept for compatibility.
ErrUnimplemented = errors.New("unimplemented")
+ ErrChunkTypeNotHeartbeatAck = errors.New("chunk type is not of type HEARTBEAT ACK")
ErrHeartbeatAckParams = errors.New("heartbeat Ack must have one param")
ErrHeartbeatAckNotHeartbeatInfo = errors.New("heartbeat Ack must have one param, and it should be a HeartbeatInfo")
ErrHeartbeatAckMarshalParam = errors.New("unable to marshal parameter for Heartbeat Ack")
)
-func (h *chunkHeartbeatAck) unmarshal([]byte) error {
- return ErrUnimplemented
+func (h *chunkHeartbeatAck) unmarshal(raw []byte) error { //nolint:cyclop
+ if err := h.chunkHeader.unmarshal(raw); err != nil {
+ return err
+ }
+
+ if h.typ != ctHeartbeatAck {
+ return fmt.Errorf("%w %s", ErrChunkTypeNotHeartbeatAck, h.typ.String())
+ }
+
+ // allow for an empty heartbeat: no RTT info -> ActiveHeartbeat just won't update SRTT.
+ if len(h.raw) == 0 {
+ h.params = nil
+
+ return nil
+ }
+
+ if len(h.raw) < initOptionalVarHeaderLength {
+ return fmt.Errorf("%w: %d", ErrHeartbeatAckParams, len(h.raw))
+ }
+
+ pType, err := parseParamType(h.raw)
+ if err != nil {
+ return fmt.Errorf("%w: %v", ErrHeartbeatAckParams, err) //nolint:errorlint
+ }
+ if pType != heartbeatInfo {
+ return fmt.Errorf("%w: instead have %s", ErrHeartbeatAckNotHeartbeatInfo, pType.String())
+ }
+
+ var pHeader paramHeader
+ if e := pHeader.unmarshal(h.raw); e != nil {
+ return fmt.Errorf("%w: %v", ErrHeartbeatAckParams, e) //nolint:errorlint
+ }
+ plen := pHeader.length()
+ if plen < initOptionalVarHeaderLength || plen > len(h.raw) {
+ return fmt.Errorf("%w: %d", ErrHeartbeatAckParams, plen)
+ }
+
+ p, err := buildParam(pType, h.raw[:plen])
+ if err != nil {
+ return fmt.Errorf("%w: %v", ErrHeartbeatAckMarshalParam, err) //nolint:errorlint
+ }
+ h.params = []param{p}
+
+ // Any trailing bytes beyond the single param must be zero.
+ if rem := h.raw[plen:]; len(rem) > 0 && !allZero(rem) {
+ return ErrHeartbeatExtraNonZero
+ }
+
+ return nil
}
func (h *chunkHeartbeatAck) marshal() ([]byte, error) {
diff --git a/vendor/github.com/pion/sctp/chunk_init_common.go b/vendor/github.com/pion/sctp/chunk_init_common.go
index 5c893a7d79..0b0becdea8 100644
--- a/vendor/github.com/pion/sctp/chunk_init_common.go
+++ b/vendor/github.com/pion/sctp/chunk_init_common.go
@@ -166,3 +166,14 @@ func (i chunkInitCommon) String() string {
return res
}
+
+// allZero returns true if every byte is 0x00.
+func allZero(b []byte) bool {
+ for _, v := range b {
+ if v != 0 {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/vendor/github.com/pion/sctp/chunk_payload_data.go b/vendor/github.com/pion/sctp/chunk_payload_data.go
index 773b29cd2e..3e5746e997 100644
--- a/vendor/github.com/pion/sctp/chunk_payload_data.go
+++ b/vendor/github.com/pion/sctp/chunk_payload_data.go
@@ -75,6 +75,10 @@ type chunkPayloadData struct {
retransmit bool
head *chunkPayloadData // link to the head of the fragment
+
+ rackPrev *chunkPayloadData
+ rackNext *chunkPayloadData
+ rackInList bool
}
const (
diff --git a/vendor/github.com/pion/sctp/packet.go b/vendor/github.com/pion/sctp/packet.go
index 1e4ce529cb..a8e82d1035 100644
--- a/vendor/github.com/pion/sctp/packet.go
+++ b/vendor/github.com/pion/sctp/packet.go
@@ -94,16 +94,19 @@ func (p *packet) unmarshal(doChecksum bool, raw []byte) error { //nolint:cyclop
p.destinationPort = binary.BigEndian.Uint16(raw[2:])
p.verificationTag = binary.BigEndian.Uint32(raw[4:])
- for {
- // Exact match, no more chunks
- if offset == len(raw) {
- break
- } else if offset+chunkHeaderSize > len(raw) {
- return fmt.Errorf("%w: offset %d remaining %d", ErrParseSCTPChunkNotEnoughData, offset, len(raw))
+ for offset < len(raw) {
+ // guaranteed to be safe by loop condition
+ remaining := raw[offset:] // nolint:gosec
+
+ // must have at least a full chunk header to continue.
+ if len(remaining) < chunkHeaderSize {
+ return fmt.Errorf("%w: offset %d remaining %d", ErrParseSCTPChunkNotEnoughData, offset, len(remaining))
}
+ ctype := chunkType(remaining[0])
+
var dataChunk chunk
- switch chunkType(raw[offset]) {
+ switch ctype {
case ctInit:
dataChunk = &chunkInit{}
case ctInitAck:
@@ -133,10 +136,10 @@ func (p *packet) unmarshal(doChecksum bool, raw []byte) error { //nolint:cyclop
case ctShutdownComplete:
dataChunk = &chunkShutdownComplete{}
default:
- return fmt.Errorf("%w: %s", ErrUnmarshalUnknownChunkType, chunkType(raw[offset]).String())
+ return fmt.Errorf("%w: %s", ErrUnmarshalUnknownChunkType, ctype.String())
}
- if err := dataChunk.unmarshal(raw[offset:]); err != nil {
+ if err := dataChunk.unmarshal(remaining); err != nil {
return err
}
@@ -145,6 +148,21 @@ func (p *packet) unmarshal(doChecksum bool, raw []byte) error { //nolint:cyclop
offset += chunkHeaderSize + dataChunk.valueLength() + chunkValuePadding
}
+ // if we overshot then should error.
+ if offset != len(raw) {
+ if offset > len(raw) {
+ overshoot := offset - len(raw)
+
+ return fmt.Errorf("%w: parsed past end of buffer by %d bytes (offset %d, length %d)",
+ ErrParseSCTPChunkNotEnoughData, overshoot, offset, len(raw))
+ }
+
+ remaining := len(raw) - offset
+
+ return fmt.Errorf("%w: unparsed data remaining: %d bytes (offset %d, length %d)",
+ ErrParseSCTPChunkNotEnoughData, remaining, offset, len(raw))
+ }
+
return nil
}
diff --git a/vendor/github.com/pion/sctp/windowedmin.go b/vendor/github.com/pion/sctp/windowedmin.go
new file mode 100644
index 0000000000..c8a5c5fef6
--- /dev/null
+++ b/vendor/github.com/pion/sctp/windowedmin.go
@@ -0,0 +1,81 @@
+// SPDX-FileCopyrightText: 2023 The Pion community
+// SPDX-License-Identifier: MIT
+
+package sctp
+
+import (
+ "sort"
+ "time"
+)
+
+// windowedMin maintains a monotonic deque of (time,value) to answer
+// the minimum over a sliding window efficiently.
+// Not thread-safe; caller must synchronize (Association already does).
+type windowedMin struct {
+ rackMinRTTWnd time.Duration
+ deque []entry
+}
+
+type entry struct {
+ t time.Time
+ v time.Duration
+}
+
+func newWindowedMin(window time.Duration) *windowedMin {
+ if window <= 0 {
+ window = 30 * time.Second
+ }
+
+ return &windowedMin{rackMinRTTWnd: window}
+}
+
+// prune removes elements older than (now - wnd).
+func (window *windowedMin) prune(now time.Time) {
+ if len(window.deque) == 0 {
+ return
+ }
+
+ cutoff := now.Add(-window.rackMinRTTWnd)
+
+ firstValidTSAfterCutoff := sort.Search(len(window.deque), func(i int) bool {
+ return !window.deque[i].t.Before(cutoff) // no builtin func for >= cutoff time
+ })
+
+ if firstValidTSAfterCutoff > 0 {
+ window.deque = window.deque[firstValidTSAfterCutoff:]
+ }
+}
+
+// Push inserts a new sample and preserves monotonic non-increasing values.
+// It maintains minimum values by removing larger entries.
+func (window *windowedMin) Push(now time.Time, v time.Duration) {
+ window.prune(now)
+
+ for i := len(window.deque); i > 0 && window.deque[i-1].v >= v; i-- {
+ window.deque = window.deque[:i-1]
+ }
+
+ window.deque = append(
+ window.deque,
+ entry{
+ t: now,
+ v: v,
+ },
+ )
+}
+
+// Min returns the minimum value in the current window or 0 if empty.
+func (window *windowedMin) Min(now time.Time) time.Duration {
+ window.prune(now)
+
+ if len(window.deque) == 0 {
+ return 0
+ }
+
+ return window.deque[0].v
+}
+
+// Len is only for tests/diagnostics.
+func (window *windowedMin) Len() int {
+ return len(window.deque)
+}
diff --git a/vendor/github.com/pion/sdp/v3/base_lexer.go b/vendor/github.com/pion/sdp/v3/base_lexer.go
index a0fa6f7f9f..65a9f648f6 100644
--- a/vendor/github.com/pion/sdp/v3/base_lexer.go
+++ b/vendor/github.com/pion/sdp/v3/base_lexer.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
+ "slices"
"strconv"
)
@@ -228,11 +229,5 @@ func isNewline(ch byte) bool { return ch == '\n' || ch == '\r' }
func isWhitespace(ch byte) bool { return ch == ' ' || ch == '\t' }
func anyOf(element string, data ...string) bool {
- for _, v := range data {
- if element == v {
- return true
- }
- }
-
- return false
+ return slices.Contains(data, element)
}
diff --git a/vendor/github.com/pion/sdp/v3/jsep.go b/vendor/github.com/pion/sdp/v3/jsep.go
index ce0ba53abb..a0960f3c2f 100644
--- a/vendor/github.com/pion/sdp/v3/jsep.go
+++ b/vendor/github.com/pion/sdp/v3/jsep.go
@@ -117,10 +117,34 @@ func (s *SessionDescription) WithValueAttribute(key, value string) *SessionDescr
return s
}
+// addOrUpdateICEOption adds or updates the ice-options attribute with the given value.
+func (s *SessionDescription) addOrUpdateICEOption(value string) *SessionDescription {
+ for i := range s.Attributes {
+ if s.Attributes[i].Key == AttrKeyICEOptions {
+ prefix := " "
+ if s.Attributes[i].Value == "" {
+ prefix = ""
+ }
+
+ s.Attributes[i].Value += prefix + value
+
+ return s
+ }
+ }
+
+ return s.WithValueAttribute(AttrKeyICEOptions, value)
+}
+
// WithICETrickleAdvertised advertises ICE trickle support in the session description.
// See https://datatracker.ietf.org/doc/html/rfc9429#section-5.2.1
func (s *SessionDescription) WithICETrickleAdvertised() *SessionDescription {
- return s.WithValueAttribute(AttrKeyICEOptions, "trickle")
+ return s.addOrUpdateICEOption("trickle")
+}
+
+// WithICERenomination advertises ICE renomination support in the session description.
+// See https://datatracker.ietf.org/doc/html/draft-thatcher-ice-renomination-01#section-3
+func (s *SessionDescription) WithICERenomination() *SessionDescription {
+ return s.addOrUpdateICEOption("renomination")
}
// WithFingerprint adds a fingerprint to the session description.
diff --git a/vendor/github.com/pion/sdp/v3/util.go b/vendor/github.com/pion/sdp/v3/util.go
index 5668873752..2a1c96d012 100644
--- a/vendor/github.com/pion/sdp/v3/util.go
+++ b/vendor/github.com/pion/sdp/v3/util.go
@@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"io"
+ "slices"
"sort"
"strconv"
"strings"
@@ -98,10 +99,8 @@ func (c Codec) String() string {
}
func (c *Codec) appendRTCPFeedback(rtcpFeedback string) {
- for _, existingRTCPFeedback := range c.RTCPFeedback {
- if existingRTCPFeedback == rtcpFeedback {
- return
- }
+ if slices.Contains(c.RTCPFeedback, rtcpFeedback) {
+ return
}
c.RTCPFeedback = append(c.RTCPFeedback, rtcpFeedback)
@@ -337,6 +336,20 @@ func (s *SessionDescription) GetCodecForPayloadType(payloadType uint8) (Codec, e
return codec, errPayloadTypeNotFound
}
+func (s *SessionDescription) GetCodecsForPayloadTypes(payloadTypes []uint8) ([]Codec, error) {
+ codecs := s.buildCodecMap()
+
+ result := make([]Codec, 0, len(payloadTypes))
+ for _, payloadType := range payloadTypes {
+ codec, ok := codecs[payloadType]
+ if ok {
+ result = append(result, codec)
+ }
+ }
+
+ return result, nil
+}
+
// GetPayloadTypeForCodec scans the SessionDescription for a codec that matches the provided codec
// as closely as possible and returns its payload type.
func (s *SessionDescription) GetPayloadTypeForCodec(wanted Codec) (uint8, error) {
diff --git a/vendor/github.com/pion/webrtc/v4/dtlstransport_js.go b/vendor/github.com/pion/webrtc/v4/dtlstransport_js.go
index 846cfb7127..f0c40ca41e 100644
--- a/vendor/github.com/pion/webrtc/v4/dtlstransport_js.go
+++ b/vendor/github.com/pion/webrtc/v4/dtlstransport_js.go
@@ -34,3 +34,32 @@ func (r *DTLSTransport) ICETransport() *ICETransport {
underlying: underlying,
}
}
+
+func (t *DTLSTransport) GetRemoteCertificate() []byte {
+ if t.underlying.IsNull() || t.underlying.IsUndefined() {
+ return nil
+ }
+
+ // Firefox does not support getRemoteCertificates: https://bugzilla.mozilla.org/show_bug.cgi?id=1805446
+ jsGet := t.underlying.Get("getRemoteCertificates")
+ if jsGet.IsUndefined() || jsGet.IsNull() {
+ return nil
+ }
+
+ jsCerts := t.underlying.Call("getRemoteCertificates")
+ if jsCerts.Length() == 0 {
+ return nil
+ }
+
+ buf := jsCerts.Index(0)
+ u8 := js.Global().Get("Uint8Array").New(buf)
+
+ if u8.Length() == 0 {
+ return nil
+ }
+
+ cert := make([]byte, u8.Length())
+ js.CopyBytesToGo(cert, u8)
+
+ return cert
+}
diff --git a/vendor/github.com/pion/webrtc/v4/errors.go b/vendor/github.com/pion/webrtc/v4/errors.go
index acece24df2..15535f6d16 100644
--- a/vendor/github.com/pion/webrtc/v4/errors.go
+++ b/vendor/github.com/pion/webrtc/v4/errors.go
@@ -199,6 +199,7 @@ var (
errICERoleUnknown = errors.New("unknown ICE Role")
errICEProtocolUnknown = errors.New("unknown protocol")
errICEGathererNotStarted = errors.New("gatherer not started")
+ errAddressRewriteWithNAT1To1 = errors.New("address rewrite rules cannot be combined with NAT1To1IPs")
errNetworkTypeUnknown = errors.New("unknown network type")
diff --git a/vendor/github.com/pion/webrtc/v4/icecandidatetype.go b/vendor/github.com/pion/webrtc/v4/icecandidatetype.go
index 7ac62db119..8a5fe93380 100644
--- a/vendor/github.com/pion/webrtc/v4/icecandidatetype.go
+++ b/vendor/github.com/pion/webrtc/v4/icecandidatetype.go
@@ -101,7 +101,7 @@ func getCandidateType(candidateType ice.CandidateType) (ICECandidateType, error)
}
// MarshalText implements the encoding.TextMarshaler interface.
-func (t ICECandidateType) MarshalText() ([]byte, error) {
+func (t ICECandidateType) MarshalText() ([]byte, error) { //nolint:staticcheck
return []byte(t.String()), nil
}
@@ -112,3 +112,7 @@ func (t *ICECandidateType) UnmarshalText(b []byte) error {
return err
}
+
+func (r ICECandidateType) toICE() ice.CandidateType {
+ return ice.CandidateType(r)
+}
diff --git a/vendor/github.com/pion/webrtc/v4/icegatherer.go b/vendor/github.com/pion/webrtc/v4/icegatherer.go
index 2a1bc6b4e0..0fe7e876d4 100644
--- a/vendor/github.com/pion/webrtc/v4/icegatherer.go
+++ b/vendor/github.com/pion/webrtc/v4/icegatherer.go
@@ -8,8 +8,10 @@ package webrtc
import (
"fmt"
+ "strings"
"sync"
"sync/atomic"
+ "time"
"github.com/pion/ice/v4"
"github.com/pion/logging"
@@ -44,6 +46,48 @@ type ICEGatherer struct {
sdpMLineIndex atomic.Uint32 // uint16
}
+// ICEAddressRewriteMode controls whether a rule replaces or appends candidates.
+type ICEAddressRewriteMode byte
+
+const (
+ ICEAddressRewriteModeUnspecified ICEAddressRewriteMode = iota
+ ICEAddressRewriteReplace
+ ICEAddressRewriteAppend
+)
+
+func (r ICEAddressRewriteMode) toICE() ice.AddressRewriteMode {
+ return ice.AddressRewriteMode(r)
+}
+
+// ICEAddressRewriteRule represents a rule for remapping candidate addresses.
+type ICEAddressRewriteRule struct {
+ External []string
+ Local string
+ Iface string
+ CIDR string
+ AsCandidateType ICECandidateType
+ Mode ICEAddressRewriteMode
+ Networks []NetworkType
+}
+
+func (r ICEAddressRewriteRule) toICE() ice.AddressRewriteRule {
+ candidateType := r.AsCandidateType.toICE()
+ mode := r.Mode.toICE()
+ networks := toICENetworkTypes(r.Networks)
+
+ rule := ice.AddressRewriteRule{
+ External: append([]string(nil), r.External...),
+ Local: r.Local,
+ Iface: r.Iface,
+ CIDR: r.CIDR,
+ AsCandidateType: candidateType,
+ Mode: mode,
+ Networks: networks,
+ }
+
+ return rule
+}
+
// NewICEGatherer creates a new NewICEGatherer.
// This constructor is part of the ORTC API. It is not
// meant to be used together with the basic WebRTC API.
@@ -70,7 +114,7 @@ func (api *API) NewICEGatherer(opts ICEGatherOptions) (*ICEGatherer, error) {
}, nil
}
-func (g *ICEGatherer) createAgent() error { //nolint:cyclop
+func (g *ICEGatherer) createAgent() error {
g.lock.Lock()
defer g.lock.Unlock()
@@ -78,79 +122,245 @@ func (g *ICEGatherer) createAgent() error { //nolint:cyclop
return nil
}
- candidateTypes := []ice.CandidateType{}
+ options, err := g.buildAgentOptions()
+ if err != nil {
+ return err
+ }
+
+ agent, err := ice.NewAgentWithOptions(options...)
+ if err != nil {
+ return err
+ }
+
+ g.agent = agent
+
+ return nil
+}
+
+func (g *ICEGatherer) buildAgentOptions() ([]ice.AgentOption, error) {
+ candidateTypes := g.resolveCandidateTypes()
+ nat1To1CandiTyp := g.resolveNAT1To1CandidateType()
+ mDNSMode := g.sanitizedMDNSMode()
+
+ options := g.baseAgentOptions(mDNSMode)
+ if len(candidateTypes) > 0 {
+ options = append(options, ice.WithCandidateTypes(candidateTypes))
+ }
+
+ options = append(options, g.credentialOptions()...)
+
+ rewriteOptions, err := g.addressRewriteOptions(nat1To1CandiTyp)
+ if err != nil {
+ return nil, err
+ }
+ options = append(options, rewriteOptions...)
+ options = append(options, g.timeoutOptions()...)
+ options = append(options, g.miscOptions()...)
+ options = append(options, g.renominationOptions()...)
+
+ requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes
+ if len(requestedNetworkTypes) == 0 {
+ requestedNetworkTypes = supportedNetworkTypes()
+ }
+
+ return append(options, ice.WithNetworkTypes(toICENetworkTypes(requestedNetworkTypes))), nil
+}
+
+func (g *ICEGatherer) resolveCandidateTypes() []ice.CandidateType {
if g.api.settingEngine.candidates.ICELite {
- candidateTypes = append(candidateTypes, ice.CandidateTypeHost)
- } else if g.gatherPolicy == ICETransportPolicyRelay {
- candidateTypes = append(candidateTypes, ice.CandidateTypeRelay)
+ return []ice.CandidateType{ice.CandidateTypeHost}
+ }
+
+ switch g.gatherPolicy {
+ case ICETransportPolicyRelay:
+ return []ice.CandidateType{ice.CandidateTypeRelay}
+ case ICETransportPolicyNoHost:
+ return []ice.CandidateType{ice.CandidateTypeServerReflexive, ice.CandidateTypeRelay}
+ default:
}
- var nat1To1CandiTyp ice.CandidateType
+ return nil
+}
+
+func (g *ICEGatherer) resolveNAT1To1CandidateType() ice.CandidateType {
switch g.api.settingEngine.candidates.NAT1To1IPCandidateType {
case ICECandidateTypeHost:
- nat1To1CandiTyp = ice.CandidateTypeHost
+ return ice.CandidateTypeHost
case ICECandidateTypeSrflx:
- nat1To1CandiTyp = ice.CandidateTypeServerReflexive
+ return ice.CandidateTypeServerReflexive
default:
- nat1To1CandiTyp = ice.CandidateTypeUnspecified
- }
-
- mDNSMode := g.api.settingEngine.candidates.MulticastDNSMode
- if mDNSMode != ice.MulticastDNSModeDisabled && mDNSMode != ice.MulticastDNSModeQueryAndGather {
- // If enum is in state we don't recognized default to MulticastDNSModeQueryOnly
- mDNSMode = ice.MulticastDNSModeQueryOnly
- }
-
- config := &ice.AgentConfig{
- Lite: g.api.settingEngine.candidates.ICELite,
- Urls: g.validatedServers,
- PortMin: g.api.settingEngine.ephemeralUDP.PortMin,
- PortMax: g.api.settingEngine.ephemeralUDP.PortMax,
- DisconnectedTimeout: g.api.settingEngine.timeout.ICEDisconnectedTimeout,
- FailedTimeout: g.api.settingEngine.timeout.ICEFailedTimeout,
- KeepaliveInterval: g.api.settingEngine.timeout.ICEKeepaliveInterval,
- LoggerFactory: g.api.settingEngine.LoggerFactory,
- CandidateTypes: candidateTypes,
- HostAcceptanceMinWait: g.api.settingEngine.timeout.ICEHostAcceptanceMinWait,
- SrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait,
- PrflxAcceptanceMinWait: g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait,
- RelayAcceptanceMinWait: g.api.settingEngine.timeout.ICERelayAcceptanceMinWait,
- STUNGatherTimeout: g.api.settingEngine.timeout.ICESTUNGatherTimeout,
- InterfaceFilter: g.api.settingEngine.candidates.InterfaceFilter,
- IPFilter: g.api.settingEngine.candidates.IPFilter,
- NAT1To1IPs: g.api.settingEngine.candidates.NAT1To1IPs,
- NAT1To1IPCandidateType: nat1To1CandiTyp,
- IncludeLoopback: g.api.settingEngine.candidates.IncludeLoopbackCandidate,
- Net: g.api.settingEngine.net,
- MulticastDNSMode: mDNSMode,
- MulticastDNSHostName: g.api.settingEngine.candidates.MulticastDNSHostName,
- LocalUfrag: g.api.settingEngine.candidates.UsernameFragment,
- LocalPwd: g.api.settingEngine.candidates.Password,
- TCPMux: g.api.settingEngine.iceTCPMux,
- UDPMux: g.api.settingEngine.iceUDPMux,
- ProxyDialer: g.api.settingEngine.iceProxyDialer,
- DisableActiveTCP: g.api.settingEngine.iceDisableActiveTCP,
- MaxBindingRequests: g.api.settingEngine.iceMaxBindingRequests,
- BindingRequestHandler: g.api.settingEngine.iceBindingRequestHandler,
+ return ice.CandidateTypeUnspecified
}
+}
- requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes
- if len(requestedNetworkTypes) == 0 {
- requestedNetworkTypes = supportedNetworkTypes()
+func (g *ICEGatherer) sanitizedMDNSMode() ice.MulticastDNSMode {
+ mode := g.api.settingEngine.candidates.MulticastDNSMode
+ if mode == ice.MulticastDNSModeDisabled || mode == ice.MulticastDNSModeQueryAndGather {
+ return mode
}
- for _, typ := range requestedNetworkTypes {
- config.NetworkTypes = append(config.NetworkTypes, ice.NetworkType(typ))
+ return ice.MulticastDNSModeQueryOnly
+}
+
+func (g *ICEGatherer) baseAgentOptions(mDNSMode ice.MulticastDNSMode) []ice.AgentOption {
+ return []ice.AgentOption{
+ ice.WithICELite(g.api.settingEngine.candidates.ICELite),
+ ice.WithUrls(g.validatedServers),
+ ice.WithPortRange(g.api.settingEngine.ephemeralUDP.PortMin, g.api.settingEngine.ephemeralUDP.PortMax),
+ ice.WithLoggerFactory(g.api.settingEngine.LoggerFactory),
+ ice.WithInterfaceFilter(g.api.settingEngine.candidates.InterfaceFilter),
+ ice.WithIPFilter(g.api.settingEngine.candidates.IPFilter),
+ ice.WithNet(g.api.settingEngine.net),
+ ice.WithMulticastDNSMode(mDNSMode),
+ ice.WithTCPMux(g.api.settingEngine.iceTCPMux),
+ ice.WithUDPMux(g.api.settingEngine.iceUDPMux),
+ ice.WithProxyDialer(g.api.settingEngine.iceProxyDialer),
+ ice.WithBindingRequestHandler(g.api.settingEngine.iceBindingRequestHandler),
}
+}
- agent, err := ice.NewAgent(config)
- if err != nil {
- return err
+func (g *ICEGatherer) credentialOptions() []ice.AgentOption {
+ ufrag := g.api.settingEngine.candidates.UsernameFragment
+ pass := g.api.settingEngine.candidates.Password
+ if ufrag == "" && pass == "" {
+ return nil
}
- g.agent = agent
+ return []ice.AgentOption{
+ ice.WithLocalCredentials(g.api.settingEngine.candidates.UsernameFragment, g.api.settingEngine.candidates.Password),
+ }
+}
- return nil
+func (g *ICEGatherer) addressRewriteOptions(candidateType ice.CandidateType) ([]ice.AgentOption, error) {
+ rules := g.api.settingEngine.candidates.addressRewriteRules
+ nat1To1IPs := g.api.settingEngine.candidates.NAT1To1IPs
+ if len(rules) > 0 && len(nat1To1IPs) > 0 {
+ return nil, errAddressRewriteWithNAT1To1
+ }
+
+ if len(rules) > 0 {
+ return []ice.AgentOption{ice.WithAddressRewriteRules(rules...)}, nil
+ }
+
+ if len(nat1To1IPs) == 0 {
+ return nil, nil
+ }
+
+ return []ice.AgentOption{
+ ice.WithAddressRewriteRules(
+ legacyNAT1To1AddressRewriteRules(
+ nat1To1IPs,
+ candidateType,
+ )...,
+ ),
+ }, nil
+}
+
+func (g *ICEGatherer) timeoutOptions() []ice.AgentOption {
+ opts := make([]ice.AgentOption, 0, 8)
+
+ if g.api.settingEngine.timeout.ICEDisconnectedTimeout != nil {
+ opts = append(opts, ice.WithDisconnectedTimeout(*g.api.settingEngine.timeout.ICEDisconnectedTimeout))
+ }
+ if g.api.settingEngine.timeout.ICEFailedTimeout != nil {
+ opts = append(opts, ice.WithFailedTimeout(*g.api.settingEngine.timeout.ICEFailedTimeout))
+ }
+ if g.api.settingEngine.timeout.ICEKeepaliveInterval != nil {
+ opts = append(opts, ice.WithKeepaliveInterval(*g.api.settingEngine.timeout.ICEKeepaliveInterval))
+ }
+ if g.api.settingEngine.timeout.ICEHostAcceptanceMinWait != nil {
+ opts = append(opts, ice.WithHostAcceptanceMinWait(*g.api.settingEngine.timeout.ICEHostAcceptanceMinWait))
+ }
+ if g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait != nil {
+ opts = append(opts, ice.WithSrflxAcceptanceMinWait(*g.api.settingEngine.timeout.ICESrflxAcceptanceMinWait))
+ }
+ if g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait != nil {
+ opts = append(opts, ice.WithPrflxAcceptanceMinWait(*g.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait))
+ }
+ if g.api.settingEngine.timeout.ICERelayAcceptanceMinWait != nil {
+ opts = append(opts, ice.WithRelayAcceptanceMinWait(*g.api.settingEngine.timeout.ICERelayAcceptanceMinWait))
+ }
+ if g.api.settingEngine.timeout.ICESTUNGatherTimeout != nil {
+ opts = append(opts, ice.WithSTUNGatherTimeout(*g.api.settingEngine.timeout.ICESTUNGatherTimeout))
+ }
+
+ return opts
+}
+
+func (g *ICEGatherer) miscOptions() []ice.AgentOption {
+ opts := make([]ice.AgentOption, 0, 4)
+
+ if g.api.settingEngine.candidates.MulticastDNSHostName != "" {
+ opts = append(opts, ice.WithMulticastDNSHostName(g.api.settingEngine.candidates.MulticastDNSHostName))
+ }
+
+ if g.api.settingEngine.candidates.IncludeLoopbackCandidate {
+ opts = append(opts, ice.WithIncludeLoopback())
+ }
+
+ if g.api.settingEngine.iceDisableActiveTCP {
+ opts = append(opts, ice.WithDisableActiveTCP())
+ }
+
+ if g.api.settingEngine.iceMaxBindingRequests != nil {
+ opts = append(opts, ice.WithMaxBindingRequests(*g.api.settingEngine.iceMaxBindingRequests))
+ }
+
+ return opts
+}
+
+func (g *ICEGatherer) renominationOptions() []ice.AgentOption {
+ renom := g.api.settingEngine.renomination
+ if !renom.enabled && !renom.automatic {
+ return nil
+ }
+
+ generator := renom.generator
+ opts := []ice.AgentOption{
+ ice.WithRenomination(func() uint32 {
+ return generator()
+ }),
+ }
+
+ if renom.automatic {
+ interval := time.Duration(0)
+ if renom.automaticInterval != nil {
+ interval = *renom.automaticInterval
+ }
+
+ opts = append(opts, ice.WithAutomaticRenomination(interval))
+ }
+
+ return opts
+}
+
+func legacyNAT1To1AddressRewriteRules(ips []string, candidateType ice.CandidateType) []ice.AddressRewriteRule {
+ catchAll := make([]string, 0, len(ips))
+ rules := make([]ice.AddressRewriteRule, 0, len(ips)+1)
+
+ for _, ip := range ips {
+ splits := strings.SplitN(ip, "/", 2)
+
+ if len(splits) == 2 {
+ rules = append(rules, ice.AddressRewriteRule{
+ External: []string{splits[0]},
+ Local: splits[1],
+ AsCandidateType: candidateType,
+ })
+ catchAll = append(catchAll, splits[0])
+ } else {
+ catchAll = append(catchAll, ip)
+ }
+ }
+
+ if len(catchAll) > 0 {
+ rules = append(rules, ice.AddressRewriteRule{
+ External: catchAll,
+ AsCandidateType: candidateType,
+ })
+ }
+
+ return rules
}
// Gather ICE candidates.
diff --git a/vendor/github.com/pion/webrtc/v4/icetransportpolicy.go b/vendor/github.com/pion/webrtc/v4/icetransportpolicy.go
index 39a1fa364a..9aa8cb0f2d 100644
--- a/vendor/github.com/pion/webrtc/v4/icetransportpolicy.go
+++ b/vendor/github.com/pion/webrtc/v4/icetransportpolicy.go
@@ -21,17 +21,23 @@ const (
// ICETransportPolicyRelay indicates only media relay candidates such
// as candidates passing through a TURN server are used.
ICETransportPolicyRelay
+
+ // ICETransportPolicyNoHost indicates only non-host candidates are used.
+ ICETransportPolicyNoHost
)
// This is done this way because of a linter.
const (
- iceTransportPolicyRelayStr = "relay"
- iceTransportPolicyAllStr = "all"
+ iceTransportPolicyRelayStr = "relay"
+ iceTransportPolicyNoHostStr = "nohost"
+ iceTransportPolicyAllStr = "all"
)
// NewICETransportPolicy takes a string and converts it to ICETransportPolicy.
func NewICETransportPolicy(raw string) ICETransportPolicy {
switch raw {
+ case iceTransportPolicyNoHostStr:
+ return ICETransportPolicyNoHost
case iceTransportPolicyRelayStr:
return ICETransportPolicyRelay
default:
@@ -41,6 +47,8 @@ func NewICETransportPolicy(raw string) ICETransportPolicy {
func (t ICETransportPolicy) String() string {
switch t {
+ case ICETransportPolicyNoHost:
+ return iceTransportPolicyNoHostStr
case ICETransportPolicyRelay:
return iceTransportPolicyRelayStr
case ICETransportPolicyAll:
diff --git a/vendor/github.com/pion/webrtc/v4/internal/mux/endpoint.go b/vendor/github.com/pion/webrtc/v4/internal/mux/endpoint.go
index 790ded811a..33088bafe2 100644
--- a/vendor/github.com/pion/webrtc/v4/internal/mux/endpoint.go
+++ b/vendor/github.com/pion/webrtc/v4/internal/mux/endpoint.go
@@ -70,31 +70,34 @@ func (e *Endpoint) WriteTo(p []byte, _ net.Addr) (int, error) {
return e.Write(p)
}
-// LocalAddr is a stub.
+// LocalAddr returns the local network address, if known.
func (e *Endpoint) LocalAddr() net.Addr {
return e.mux.nextConn.LocalAddr()
}
-// RemoteAddr is a stub.
+// RemoteAddr returns the remote network address, if known.
func (e *Endpoint) RemoteAddr() net.Addr {
return e.mux.nextConn.RemoteAddr()
}
-// SetDeadline sets the read deadline for this Endpoint.
-// Write deadlines are not supported because writes go directly to the shared
-// underlying connection and are non-blocking for this endpoint.
+// SetDeadline sets the read and write deadlines on the shared underlying
+// connection. Because the connection is shared, this applies to all endpoints
+// on the mux. Per-endpoint read deadlines can be set with SetReadDeadline.
func (e *Endpoint) SetDeadline(t time.Time) error {
- return e.buffer.SetReadDeadline(t)
+ return e.mux.nextConn.SetDeadline(t)
}
-// SetReadDeadline sets the read deadline for this Endpoint.
+// SetReadDeadline sets the read deadline for this Endpoint's internal
+// packet buffer. This timeout applies only to reads from this Endpoint,
+// not to the shared underlying connection.
func (e *Endpoint) SetReadDeadline(t time.Time) error {
return e.buffer.SetReadDeadline(t)
}
-// SetWriteDeadline is a stub.
-func (e *Endpoint) SetWriteDeadline(time.Time) error {
- return nil
+// SetWriteDeadline sets the write deadline on the shared underlying connection.
+// Because the connection is shared, this applies to all endpoints on the mux.
+func (e *Endpoint) SetWriteDeadline(t time.Time) error {
+ return e.mux.nextConn.SetWriteDeadline(t)
}
// SetOnClose is a user set callback that
diff --git a/vendor/github.com/pion/webrtc/v4/networktype.go b/vendor/github.com/pion/webrtc/v4/networktype.go
index a7ee773c12..1cd6fe3214 100644
--- a/vendor/github.com/pion/webrtc/v4/networktype.go
+++ b/vendor/github.com/pion/webrtc/v4/networktype.go
@@ -62,7 +62,7 @@ func (t NetworkType) String() string {
}
// Protocol returns udp or tcp.
-func (t NetworkType) Protocol() string {
+func (t NetworkType) Protocol() string { //nolint:staticcheck
switch t {
case NetworkTypeUDP4:
return "udp"
@@ -108,3 +108,20 @@ func getNetworkType(iceNetworkType ice.NetworkType) (NetworkType, error) {
return NetworkTypeUnknown, fmt.Errorf("%w: %s", errNetworkTypeUnknown, iceNetworkType.String())
}
}
+
+func toICENetworkTypes(networkTypes []NetworkType) []ice.NetworkType {
+ if len(networkTypes) == 0 {
+ return nil
+ }
+
+ converted := make([]ice.NetworkType, 0, len(networkTypes))
+ for _, networkType := range networkTypes {
+ converted = append(converted, networkType.toICE())
+ }
+
+ return converted
+}
+
+func (networkType NetworkType) toICE() ice.NetworkType {
+ return ice.NetworkType(networkType)
+}
diff --git a/vendor/github.com/pion/webrtc/v4/peerconnection.go b/vendor/github.com/pion/webrtc/v4/peerconnection.go
index d6ba18eb28..d63e4a816d 100644
--- a/vendor/github.com/pion/webrtc/v4/peerconnection.go
+++ b/vendor/github.com/pion/webrtc/v4/peerconnection.go
@@ -724,6 +724,9 @@ func (pc *PeerConnection) CreateOffer(options *OfferOptions) (SessionDescription
if options != nil && options.ICETricklingSupported {
descr.WithICETrickleAdvertised()
}
+ if pc.api.settingEngine.renomination.enabled {
+ descr.WithICERenomination()
+ }
updateSDPOrigin(&pc.sdpOrigin, descr)
sdpBytes, err := descr.Marshal()
@@ -892,6 +895,9 @@ func (pc *PeerConnection) CreateAnswer(options *AnswerOptions) (SessionDescripti
if options != nil && options.ICETricklingSupported {
descr.WithICETrickleAdvertised()
}
+ if pc.api.settingEngine.renomination.enabled {
+ descr.WithICERenomination()
+ }
updateSDPOrigin(&pc.sdpOrigin, descr)
sdpBytes, err := descr.Marshal()
@@ -1875,9 +1881,6 @@ func (pc *PeerConnection) handleIncomingSSRC(rtpStream *srtp.ReadStreamSRTP, ssr
}
if rsid != "" {
- receiver.mu.Lock()
- defer receiver.mu.Unlock()
-
return receiver.receiveForRtx(SSRC(0), rsid, streamInfo, readStream, interceptor, rtcpReadStream, rtcpInterceptor)
}
diff --git a/vendor/github.com/pion/webrtc/v4/rtpreceiver.go b/vendor/github.com/pion/webrtc/v4/rtpreceiver.go
index d627668a6f..6eae8cee33 100644
--- a/vendor/github.com/pion/webrtc/v4/rtpreceiver.go
+++ b/vendor/github.com/pion/webrtc/v4/rtpreceiver.go
@@ -257,7 +257,7 @@ func (r *RTPReceiver) startReceive(parameters RTPReceiveParameters) error { //no
rtcpReadStream := result.rtcpReadStream
rtcpInterceptor := result.rtcpInterceptor
- if err = r.receiveForRtx(
+ if err = r.receiveForRtxInternal(
rtxSsrc,
"",
streamInfo,
@@ -552,6 +552,10 @@ func (r *RTPReceiver) receiveForRid(
r.mu.Lock()
defer r.mu.Unlock()
+ if r.haveClosed() {
+ return nil, io.EOF
+ }
+
for i := range r.tracks {
if r.tracks[i].track.RID() == rid {
r.tracks[i].track.mu.Lock()
@@ -576,8 +580,6 @@ func (r *RTPReceiver) receiveForRid(
}
// receiveForRtx starts a routine that processes the repair stream.
-//
-//nolint:cyclop
func (r *RTPReceiver) receiveForRtx(
ssrc SSRC,
rsid string,
@@ -587,6 +589,34 @@ func (r *RTPReceiver) receiveForRtx(
rtcpReadStream *srtp.ReadStreamSRTCP,
rtcpInterceptor interceptor.RTCPReader,
) error {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+
+ return r.receiveForRtxInternal(
+ ssrc,
+ rsid,
+ streamInfo,
+ rtpReadStream,
+ rtpInterceptor,
+ rtcpReadStream,
+ rtcpInterceptor,
+ )
+}
+
+//nolint:gocognit,cyclop
+func (r *RTPReceiver) receiveForRtxInternal(
+ ssrc SSRC,
+ rsid string,
+ streamInfo *interceptor.StreamInfo,
+ rtpReadStream *srtp.ReadStreamSRTP,
+ rtpInterceptor interceptor.RTPReader,
+ rtcpReadStream *srtp.ReadStreamSRTCP,
+ rtcpInterceptor interceptor.RTCPReader,
+) error {
+ if r.haveClosed() {
+ return io.EOF
+ }
+
var track *trackStreams
if ssrc != 0 && len(r.tracks) == 1 {
track = &r.tracks[0]
@@ -712,7 +742,7 @@ func (r *RTPReceiver) setRTPReadDeadline(deadline time.Time, reader *TrackRemote
// readRTX returns an RTX packet if one is available on the RTX track, otherwise returns nil.
func (r *RTPReceiver) readRTX(reader *TrackRemote) *rtxPacketWithAttributes {
- if !reader.HasRTX() {
+ if !reader.HasRTX() || r.haveClosed() {
return nil
}
diff --git a/vendor/github.com/pion/webrtc/v4/sctptransport.go b/vendor/github.com/pion/webrtc/v4/sctptransport.go
index 33eb6e9b12..32617bbcc6 100644
--- a/vendor/github.com/pion/webrtc/v4/sctptransport.go
+++ b/vendor/github.com/pion/webrtc/v4/sctptransport.go
@@ -190,6 +190,17 @@ func (r *SCTPTransport) acceptDataChannels(
}
ACCEPT:
for {
+ // check if the association has been stopped before calling accept.
+ r.lock.RLock()
+ currentAssoc := r.sctpAssociation
+ shouldStop := currentAssoc == nil || currentAssoc != assoc
+ r.lock.RUnlock()
+ if shouldStop {
+ r.onClose(nil)
+
+ return
+ }
+
dc, err := datachannel.Accept(assoc, &datachannel.Config{
LoggerFactory: r.api.settingEngine.LoggerFactory,
}, dataChannels...)
diff --git a/vendor/github.com/pion/webrtc/v4/sdp.go b/vendor/github.com/pion/webrtc/v4/sdp.go
index 106f9d14c9..3a710772fb 100644
--- a/vendor/github.com/pion/webrtc/v4/sdp.go
+++ b/vendor/github.com/pion/webrtc/v4/sdp.go
@@ -924,7 +924,7 @@ type identifiedMediaDescription struct {
SDPMLineIndex uint16
}
-func extractICEDetailsFromMedia(
+func extractICEDetailsFromMedia( //nolint:cyclop
media *identifiedMediaDescription,
log logging.LeveledLogger,
) (string, string, []ICECandidate, error) {
@@ -939,26 +939,54 @@ func extractICEDetailsFromMedia(
if pwd, havePwd := descr.Attribute("ice-pwd"); havePwd {
remotePwd = pwd
}
- for _, a := range descr.Attributes {
- if a.IsICECandidate() {
- c, err := ice.UnmarshalCandidate(a.Value)
- if err != nil {
- if errors.Is(err, ice.ErrUnknownCandidateTyp) || errors.Is(err, ice.ErrDetermineNetworkType) {
- log.Warnf("Discarding remote candidate: %s", err)
- continue
+ // track the last error we saw while parsing candidates.
+ // if we end up with no valid candidates then return prevErr.
+ var prevErr error
+
+ for _, attr := range descr.Attributes {
+ if !attr.IsICECandidate() {
+ continue
+ }
+
+ cand, err := ice.UnmarshalCandidate(attr.Value)
+ if err != nil {
+ // similar to AddICECandidate
+ if errors.Is(err, ice.ErrUnknownCandidateTyp) || errors.Is(err, ice.ErrDetermineNetworkType) {
+ if log != nil {
+ log.Warnf("Discarding remote candidate: %s", err)
}
- return "", "", nil, err
+ continue
}
- candidate, err := newICECandidateFromICE(c, media.SDPMid, media.SDPMLineIndex)
- if err != nil {
- return "", "", nil, err
+ if log != nil {
+ log.Warnf("Failed to parse remote candidate %q: %v", attr.Value, err)
}
- candidates = append(candidates, candidate)
+ prevErr = err
+
+ continue
}
+
+ candidate, err := newICECandidateFromICE(cand, media.SDPMid, media.SDPMLineIndex)
+ if err != nil {
+ if log != nil {
+ log.Warnf("Failed to convert remote candidate %q: %v", attr.Value, err)
+ }
+
+ prevErr = err
+
+ continue
+ }
+
+ candidates = append(candidates, candidate)
+ }
+
+ // if we saw only invalid candidates then bubble up the last error
+ // so SetRemoteDescription fails with prevErr.
+ if len(candidates) == 0 && prevErr != nil {
+ return "", "", nil, prevErr
}
return remoteUfrag, remotePwd, candidates, nil
diff --git a/vendor/github.com/pion/webrtc/v4/settingengine.go b/vendor/github.com/pion/webrtc/v4/settingengine.go
index 63aa59d125..f513cfedf7 100644
--- a/vendor/github.com/pion/webrtc/v4/settingengine.go
+++ b/vendor/github.com/pion/webrtc/v4/settingengine.go
@@ -9,6 +9,7 @@ package webrtc
import (
"context"
"crypto/x509"
+ "errors"
"io"
"net"
"time"
@@ -45,13 +46,15 @@ type SettingEngine struct {
ICERelayAcceptanceMinWait *time.Duration
ICESTUNGatherTimeout *time.Duration
}
- candidates struct {
+ renomination renominationSettings
+ candidates struct {
ICELite bool
ICENetworkTypes []NetworkType
InterfaceFilter func(string) (keep bool)
IPFilter func(net.IP) (keep bool)
NAT1To1IPs []string
NAT1To1IPCandidateType ICECandidateType
+ addressRewriteRules []ice.AddressRewriteRule
MulticastDNSMode ice.MulticastDNSMode
MulticastDNSHostName string
UsernameFragment string
@@ -114,6 +117,68 @@ type SettingEngine struct {
ignoreRidPauseForRecv bool
}
+type renominationSettings struct {
+ enabled bool
+ generator ice.NominationValueGenerator
+ automatic bool
+ automaticInterval *time.Duration
+}
+
+// NominationValueGenerator generates nomination values for ICE renomination.
+type NominationValueGenerator func() uint32
+
+func (f NominationValueGenerator) toIce() ice.NominationValueGenerator {
+ return ice.NominationValueGenerator(f)
+}
+
+// RenominationOption allows configuring ICE renomination behavior.
+type RenominationOption func(*renominationSettings)
+
+// WithRenominationGenerator overrides the default nomination value generator.
+func WithRenominationGenerator(generator NominationValueGenerator) RenominationOption {
+ return func(cfg *renominationSettings) {
+ cfg.generator = generator.toIce()
+ }
+}
+
+// WithRenominationInterval sets the interval for automatic renomination checks.
+// Passing zero or a negative duration returns an error from SetICERenomination.
+func WithRenominationInterval(interval time.Duration) RenominationOption {
+ return func(cfg *renominationSettings) {
+ i := interval
+ cfg.automaticInterval = &i
+ }
+}
+
+var errInvalidRenominationInterval = errors.New("renomination interval must be greater than zero")
+
+// SetICERenomination configures ICE renomination using options for generator and scheduling.
+// Manual control is not exposed yet. This always enables automatic renomination with the default
+// generator unless a custom one is provided.
+func (e *SettingEngine) SetICERenomination(options ...RenominationOption) error {
+ cfg := e.renomination
+ for _, opt := range options {
+ if opt != nil {
+ opt(&cfg)
+ }
+ }
+
+ if cfg.automaticInterval != nil && *cfg.automaticInterval <= 0 {
+ return errInvalidRenominationInterval
+ }
+
+ if cfg.generator == nil {
+ cfg.generator = ice.DefaultNominationValueGenerator()
+ }
+
+ e.renomination.enabled = true
+ e.renomination.generator = cfg.generator
+ e.renomination.automatic = true
+ e.renomination.automaticInterval = cfg.automaticInterval
+
+ return nil
+}
+
func (e *SettingEngine) getSCTPMaxMessageSize() uint32 {
if e.sctp.maxMessageSize != 0 {
return e.sctp.maxMessageSize
@@ -265,11 +330,43 @@ func (e *SettingEngine) SetIPFilter(filter func(net.IP) (keep bool)) {
// with the public IP. The host candidate is still available along with mDNS
// capabilities unaffected. Also, you cannot give STUN server URL at the same time.
// It will result in an error otherwise.
+//
+// Deprecated: Use SetICEAddressRewriteRules instead. To mirror the legacy
+// behavior, supply ICEAddressRewriteRule with External set to ips, AsCandidateType
+// set to candidateType, and Mode set to ICEAddressRewriteReplace for host
+// candidates or ICEAddressRewriteAppend for server reflexive candidates.
+// Or leave Mode unspecified to use the default behavior;
+// replace for host candidates and append for server reflexive candidates.
func (e *SettingEngine) SetNAT1To1IPs(ips []string, candidateType ICECandidateType) {
e.candidates.NAT1To1IPs = ips
e.candidates.NAT1To1IPCandidateType = candidateType
}
+// SetICEAddressRewriteRules configures address rewrite rules for candidate publication.
+// These rules provide fine-grained control over which local addresses are replaced or
+// supplemented with external IPs.
+// This replaces the legacy NAT1To1 settings, which will be deprecated in the future.
+func (e *SettingEngine) SetICEAddressRewriteRules(rules ...ICEAddressRewriteRule) error {
+ if len(rules) == 0 {
+ e.candidates.addressRewriteRules = nil
+
+ return nil
+ }
+
+ if len(e.candidates.NAT1To1IPs) > 0 {
+ return errAddressRewriteWithNAT1To1
+ }
+
+ converted := make([]ice.AddressRewriteRule, 0, len(rules))
+ for _, rule := range rules {
+ converted = append(converted, rule.toICE())
+ }
+
+ e.candidates.addressRewriteRules = converted
+
+ return nil
+}
+
// SetIncludeLoopbackCandidate enable pion to gather loopback candidates, it is useful
// for some VM have public IP mapped to loopback interface.
func (e *SettingEngine) SetIncludeLoopbackCandidate(include bool) {
diff --git a/vendor/github.com/pion/webrtc/v4/track_local_static.go b/vendor/github.com/pion/webrtc/v4/track_local_static.go
index f590d81196..7709b56317 100644
--- a/vendor/github.com/pion/webrtc/v4/track_local_static.go
+++ b/vendor/github.com/pion/webrtc/v4/track_local_static.go
@@ -226,10 +226,12 @@ func (s *TrackLocalStaticRTP) Write(b []byte) (n int, err error) {
// TrackLocalStaticSample is a TrackLocal that has a pre-set codec and accepts Samples.
// If you wish to send a RTP Packet use TrackLocalStaticRTP.
type TrackLocalStaticSample struct {
+ mu sync.Mutex
packetizer rtp.Packetizer
sequencer rtp.Sequencer
rtpTrack *TrackLocalStaticRTP
clockRate float64
+ remainder float64
}
// NewTrackLocalStaticSample returns a TrackLocalStaticSample.
@@ -329,22 +331,36 @@ func (s *TrackLocalStaticSample) WriteSample(sample media.Sample) error {
s.rtpTrack.mu.RLock()
packetizer := s.packetizer
clockRate := s.clockRate
+ sequencer := s.sequencer
s.rtpTrack.mu.RUnlock()
-
if packetizer == nil {
return nil
}
+ s.mu.Lock()
+ remainder := s.remainder
+
// skip packets by the number of previously dropped packets
for i := uint16(0); i < sample.PrevDroppedPackets; i++ {
- s.sequencer.NextSequenceNumber()
+ sequencer.NextSequenceNumber()
}
- samples := uint32(sample.Duration.Seconds() * clockRate)
+ tickF := sample.Duration.Seconds() * clockRate
+
if sample.PrevDroppedPackets > 0 {
- packetizer.SkipSamples(samples * uint32(sample.PrevDroppedPackets))
+ dropTotal := tickF*float64(sample.PrevDroppedPackets) + remainder
+ dropTicks := uint32(dropTotal)
+ remainder = dropTotal - float64(dropTicks)
+ packetizer.SkipSamples(dropTicks)
}
- packets := packetizer.Packetize(sample.Data, samples)
+
+ curTotal := tickF + remainder
+ curTicks := uint32(curTotal)
+ remainder = curTotal - float64(curTicks)
+
+ s.remainder = remainder
+ packets := packetizer.Packetize(sample.Data, curTicks)
+ s.mu.Unlock()
writeErrs := []error{}
for _, p := range packets {
diff --git a/vendor/github.com/xujiajun/mmap-go/.gitignore b/vendor/github.com/xujiajun/mmap-go/.gitignore
deleted file mode 100644
index 0c0a5e4916..0000000000
--- a/vendor/github.com/xujiajun/mmap-go/.gitignore
+++ /dev/null
@@ -1,10 +0,0 @@
-*.out
-*.5
-*.6
-*.8
-*.swp
-_obj
-_test
-testdata
-/.idea
-*.iml
\ No newline at end of file
diff --git a/vendor/github.com/xujiajun/mmap-go/.travis.yml b/vendor/github.com/xujiajun/mmap-go/.travis.yml
deleted file mode 100644
index 169eb1f354..0000000000
--- a/vendor/github.com/xujiajun/mmap-go/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: go
-os:
- - linux
- - osx
- - windows
-go:
- - 1.11.4
-env:
- global:
- - GO111MODULE=on
-install:
- - go mod download
- - go get github.com/mattn/goveralls
-script:
- - go test -v -covermode=count -coverprofile=coverage.out -bench . -cpu 1,4
- - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN || true'
diff --git a/vendor/github.com/xujiajun/mmap-go/LICENSE b/vendor/github.com/xujiajun/mmap-go/LICENSE
deleted file mode 100644
index 8f05f338ac..0000000000
--- a/vendor/github.com/xujiajun/mmap-go/LICENSE
+++ /dev/null
@@ -1,25 +0,0 @@
-Copyright (c) 2011, Evan Shaw
-All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
- * Neither the name of the copyright holder nor the
- names of its contributors may be used to endorse or promote products
- derived from this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY
-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
diff --git a/vendor/github.com/xujiajun/mmap-go/README.md b/vendor/github.com/xujiajun/mmap-go/README.md
deleted file mode 100644
index 4cc2bfe1c8..0000000000
--- a/vendor/github.com/xujiajun/mmap-go/README.md
+++ /dev/null
@@ -1,12 +0,0 @@
-mmap-go
-=======
-
-mmap-go is a portable mmap package for the [Go programming language](http://golang.org).
-It has been tested on Linux (386, amd64), OS X, and Windows (386). It should also
-work on other Unix-like platforms, but hasn't been tested with them. I'm interested
-to hear about the results.
-
-I haven't been able to add more features without adding significant complexity,
-so mmap-go doesn't support mprotect, mincore, and maybe a few other things.
-If you're running on a Unix-like platform and need some of these features,
-I suggest Gustavo Niemeyer's [gommap](http://labix.org/gommap).
diff --git a/vendor/github.com/xujiajun/mmap-go/mmap.go b/vendor/github.com/xujiajun/mmap-go/mmap.go
deleted file mode 100644
index 29655bd222..0000000000
--- a/vendor/github.com/xujiajun/mmap-go/mmap.go
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright 2011 Evan Shaw. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// This file defines the common package interface and contains a little bit of
-// factored out logic.
-
-// Package mmap allows mapping files into memory. It tries to provide a simple, reasonably portable interface,
-// but doesn't go out of its way to abstract away every little platform detail.
-// This specifically means:
-// * forked processes may or may not inherit mappings
-// * a file's timestamp may or may not be updated by writes through mappings
-// * specifying a size larger than the file's actual size can increase the file's size
-// * If the mapped file is being modified by another process while your program's running, don't expect consistent results between platforms
-package mmap
-
-import (
- "errors"
- "os"
- "reflect"
- "unsafe"
-)
-
-const (
- // RDONLY maps the memory read-only.
- // Attempts to write to the MMap object will result in undefined behavior.
- RDONLY = 0
- // RDWR maps the memory as read-write. Writes to the MMap object will update the
- // underlying file.
- RDWR = 1 << iota
- // COPY maps the memory as copy-on-write. Writes to the MMap object will affect
- // memory, but the underlying file will remain unchanged.
- COPY
- // If EXEC is set, the mapped memory is marked as executable.
- EXEC
-)
-
-const (
- // If the ANON flag is set, the mapped memory will not be backed by a file.
- ANON = 1 << iota
-)
-
-// MMap represents a file mapped into memory.
-type MMap []byte
-
-// Map maps an entire file into memory.
-// If ANON is set in flags, f is ignored.
-func Map(f *os.File, prot, flags int) (MMap, error) {
- return MapRegion(f, -1, prot, flags, 0)
-}
-
-// MapRegion maps part of a file into memory.
-// The offset parameter must be a multiple of the system's page size.
-// If length < 0, the entire file will be mapped.
-// If ANON is set in flags, f is ignored.
-func MapRegion(f *os.File, length int, prot, flags int, offset int64) (MMap, error) {
- if offset%int64(os.Getpagesize()) != 0 {
- return nil, errors.New("offset parameter must be a multiple of the system's page size")
- }
-
- var fd uintptr
- if flags&ANON == 0 {
- fd = uintptr(f.Fd())
- if length < 0 {
- fi, err := f.Stat()
- if err != nil {
- return nil, err
- }
- length = int(fi.Size())
- }
- } else {
- if length <= 0 {
- return nil, errors.New("anonymous mapping requires non-zero length")
- }
- fd = ^uintptr(0)
- }
- return mmap(length, uintptr(prot), uintptr(flags), fd, offset)
-}
-
-func (m *MMap) header() *reflect.SliceHeader {
- return (*reflect.SliceHeader)(unsafe.Pointer(m))
-}
-
-func (m *MMap) addrLen() (uintptr, uintptr) {
- header := m.header()
- return header.Data, uintptr(header.Len)
-}
-
-// Lock keeps the mapped region in physical memory, ensuring that it will not be
-// swapped out.
-func (m MMap) Lock() error {
- return m.lock()
-}
-
-// Unlock reverses the effect of Lock, allowing the mapped region to potentially
-// be swapped out.
-// If m is already unlocked, aan error will result.
-func (m MMap) Unlock() error {
- return m.unlock()
-}
-
-// Flush synchronizes the mapping's contents to the file's contents on disk.
-func (m MMap) Flush() error {
- return m.flush()
-}
-
-// Unmap deletes the memory mapped region, flushes any remaining changes, and sets
-// m to nil.
-// Trying to read or write any remaining references to m after Unmap is called will
-// result in undefined behavior.
-// Unmap should only be called on the slice value that was originally returned from
-// a call to Map. Calling Unmap on a derived slice may cause errors.
-func (m *MMap) Unmap() error {
- err := m.unmap()
- *m = nil
- return err
-}
diff --git a/vendor/github.com/xujiajun/mmap-go/mmap_unix.go b/vendor/github.com/xujiajun/mmap-go/mmap_unix.go
deleted file mode 100644
index 25b13e51fd..0000000000
--- a/vendor/github.com/xujiajun/mmap-go/mmap_unix.go
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright 2011 Evan Shaw. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// +build darwin dragonfly freebsd linux openbsd solaris netbsd
-
-package mmap
-
-import (
- "golang.org/x/sys/unix"
-)
-
-func mmap(len int, inprot, inflags, fd uintptr, off int64) ([]byte, error) {
- flags := unix.MAP_SHARED
- prot := unix.PROT_READ
- switch {
- case inprot© != 0:
- prot |= unix.PROT_WRITE
- flags = unix.MAP_PRIVATE
- case inprot&RDWR != 0:
- prot |= unix.PROT_WRITE
- }
- if inprot&EXEC != 0 {
- prot |= unix.PROT_EXEC
- }
- if inflags&ANON != 0 {
- flags |= unix.MAP_ANON
- }
-
- b, err := unix.Mmap(int(fd), off, len, prot, flags)
- if err != nil {
- return nil, err
- }
- return b, nil
-}
-
-func (m MMap) flush() error {
- return unix.Msync([]byte(m), unix.MS_SYNC)
-}
-
-func (m MMap) lock() error {
- return unix.Mlock([]byte(m))
-}
-
-func (m MMap) unlock() error {
- return unix.Munlock([]byte(m))
-}
-
-func (m MMap) unmap() error {
- return unix.Munmap([]byte(m))
-}
diff --git a/vendor/github.com/xujiajun/mmap-go/mmap_windows.go b/vendor/github.com/xujiajun/mmap-go/mmap_windows.go
deleted file mode 100644
index 631b3825f9..0000000000
--- a/vendor/github.com/xujiajun/mmap-go/mmap_windows.go
+++ /dev/null
@@ -1,153 +0,0 @@
-// Copyright 2011 Evan Shaw. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-package mmap
-
-import (
- "errors"
- "os"
- "sync"
-
- "golang.org/x/sys/windows"
-)
-
-// mmap on Windows is a two-step process.
-// First, we call CreateFileMapping to get a handle.
-// Then, we call MapviewToFile to get an actual pointer into memory.
-// Because we want to emulate a POSIX-style mmap, we don't want to expose
-// the handle -- only the pointer. We also want to return only a byte slice,
-// not a struct, so it's convenient to manipulate.
-
-// We keep this map so that we can get back the original handle from the memory address.
-
-type addrinfo struct {
- file windows.Handle
- mapview windows.Handle
- writable bool
-}
-
-var handleLock sync.Mutex
-var handleMap = map[uintptr]*addrinfo{}
-
-func mmap(len int, prot, flags, hfile uintptr, off int64) ([]byte, error) {
- flProtect := uint32(windows.PAGE_READONLY)
- dwDesiredAccess := uint32(windows.FILE_MAP_READ)
- writable := false
- switch {
- case prot© != 0:
- flProtect = windows.PAGE_WRITECOPY
- dwDesiredAccess = windows.FILE_MAP_COPY
- writable = true
- case prot&RDWR != 0:
- flProtect = windows.PAGE_READWRITE
- dwDesiredAccess = windows.FILE_MAP_WRITE
- writable = true
- }
- if prot&EXEC != 0 {
- flProtect <<= 4
- dwDesiredAccess |= windows.FILE_MAP_EXECUTE
- }
-
- // The maximum size is the area of the file, starting from 0,
- // that we wish to allow to be mappable. It is the sum of
- // the length the user requested, plus the offset where that length
- // is starting from. This does not map the data into memory.
- maxSizeHigh := uint32((off + int64(len)) >> 32)
- maxSizeLow := uint32((off + int64(len)) & 0xFFFFFFFF)
- // TODO: Do we need to set some security attributes? It might help portability.
- h, errno := windows.CreateFileMapping(windows.Handle(hfile), nil, flProtect, maxSizeHigh, maxSizeLow, nil)
- if h == 0 {
- return nil, os.NewSyscallError("CreateFileMapping", errno)
- }
-
- // Actually map a view of the data into memory. The view's size
- // is the length the user requested.
- fileOffsetHigh := uint32(off >> 32)
- fileOffsetLow := uint32(off & 0xFFFFFFFF)
- addr, errno := windows.MapViewOfFile(h, dwDesiredAccess, fileOffsetHigh, fileOffsetLow, uintptr(len))
- if addr == 0 {
- return nil, os.NewSyscallError("MapViewOfFile", errno)
- }
- handleLock.Lock()
- handleMap[addr] = &addrinfo{
- file: windows.Handle(hfile),
- mapview: h,
- writable: writable,
- }
- handleLock.Unlock()
-
- m := MMap{}
- dh := m.header()
- dh.Data = addr
- dh.Len = len
- dh.Cap = dh.Len
-
- return m, nil
-}
-
-func (m MMap) flush() error {
- addr, len := m.addrLen()
- errno := windows.FlushViewOfFile(addr, len)
- if errno != nil {
- return os.NewSyscallError("FlushViewOfFile", errno)
- }
-
- handleLock.Lock()
- defer handleLock.Unlock()
- handle, ok := handleMap[addr]
- if !ok {
- // should be impossible; we would've errored above
- return errors.New("unknown base address")
- }
-
- if handle.writable {
- if err := windows.FlushFileBuffers(handle.file); err != nil {
- return os.NewSyscallError("FlushFileBuffers", err)
- }
- }
-
- return nil
-}
-
-func (m MMap) lock() error {
- addr, len := m.addrLen()
- errno := windows.VirtualLock(addr, len)
- return os.NewSyscallError("VirtualLock", errno)
-}
-
-func (m MMap) unlock() error {
- addr, len := m.addrLen()
- errno := windows.VirtualUnlock(addr, len)
- return os.NewSyscallError("VirtualUnlock", errno)
-}
-
-func (m MMap) unmap() error {
- err := m.flush()
- if err != nil {
- return err
- }
-
- addr := m.header().Data
- // Lock the UnmapViewOfFile along with the handleMap deletion.
- // As soon as we unmap the view, the OS is free to give the
- // same addr to another new map. We don't want another goroutine
- // to insert and remove the same addr into handleMap while
- // we're trying to remove our old addr/handle pair.
- handleLock.Lock()
- defer handleLock.Unlock()
- err = windows.UnmapViewOfFile(addr)
- if err != nil {
- return err
- }
-
- handle, ok := handleMap[addr]
- if !ok {
- // should be impossible; we would've errored above
- return errors.New("unknown base address")
- }
- delete(handleMap, addr)
-
- e := windows.CloseHandle(windows.Handle(handle.mapview))
- return os.NewSyscallError("CloseHandle", e)
-}
diff --git a/vendor/modernc.org/libc/COPYRIGHT-MUSL b/vendor/modernc.org/libc/LICENSE-3RD-PARTY.md
similarity index 59%
rename from vendor/modernc.org/libc/COPYRIGHT-MUSL
rename to vendor/modernc.org/libc/LICENSE-3RD-PARTY.md
index c1628e9ac8..04bebedb82 100644
--- a/vendor/modernc.org/libc/COPYRIGHT-MUSL
+++ b/vendor/modernc.org/libc/LICENSE-3RD-PARTY.md
@@ -1,3 +1,57 @@
+# Third-Party Software Notices
+
+This repository contains code and assets acquired from third-party sources.
+While the main project is licensed under the BSD-3 License, the components
+listed below are subject to their own specific license terms and copyright
+notices.
+
+The following is a list of third-party software included in this repository,
+their locations, and their respective licenses.
+
+
+----
+
+## Go
+
+* **URL:** https://github.com/golang/go
+----
+
+Copyright (c) 2009 The Go Authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+----
+
+## musl libc
+
+* **URL:** https://musl.libc.org/
+
+----
+
musl as a whole is licensed under the following standard MIT license:
----------------------------------------------------------------------
@@ -191,3 +245,61 @@ to be subject to copyright, resulting in confusion over whether it
negated the permissions granted in the license. In the spirit of
permissive licensing, and of not having licensing issues being an
obstacle to adoption, that text has been removed.
+
+----
+
+## go-netdb
+
+* **URL:** https://github.com/dominikh/go-netdb
+
+----
+
+Copyright (c) 2012 Dominik Honnef
+
+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 the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+----
+
+## NixOS/nixpkgs
+
+* **URL:** https://github.com/NixOS/nixpkgs
+
+----
+
+Copyright (c) 2003-2025 Eelco Dolstra and the Nixpkgs/NixOS contributors
+
+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 the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/modernc.org/libc/LICENSE-GO b/vendor/modernc.org/libc/LICENSE-GO
deleted file mode 100644
index 6a66aea5ea..0000000000
--- a/vendor/modernc.org/libc/LICENSE-GO
+++ /dev/null
@@ -1,27 +0,0 @@
-Copyright (c) 2009 The Go Authors. All rights reserved.
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are
-met:
-
- * Redistributions of source code must retain the above copyright
-notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above
-copyright notice, this list of conditions and the following disclaimer
-in the documentation and/or other materials provided with the
-distribution.
- * Neither the name of Google Inc. nor the names of its
-contributors may be used to endorse or promote products derived from
-this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/modernc.org/libc/honnef.co/go/netdb/LICENSE b/vendor/modernc.org/libc/honnef.co/go/netdb/LICENSE
deleted file mode 100644
index ddd6ddd72b..0000000000
--- a/vendor/modernc.org/libc/honnef.co/go/netdb/LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright (c) 2012 Dominik Honnef
-
-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 the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish,
-distribute, sublicense, and/or sell copies of the Software, and to
-permit persons to whom the Software is furnished to do so, subject to
-the following conditions:
-
-The above copyright notice and this permission notice shall be
-included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/modernc.org/libc/honnef.co/go/netdb/netdb.go b/vendor/modernc.org/libc/honnef.co/go/netdb/netdb.go
index dae7517216..a2bb84c53c 100644
--- a/vendor/modernc.org/libc/honnef.co/go/netdb/netdb.go
+++ b/vendor/modernc.org/libc/honnef.co/go/netdb/netdb.go
@@ -696,7 +696,8 @@ func init() {
// Load protocols
data, err := ioutil.ReadFile("/etc/protocols")
if err != nil {
- if !os.IsNotExist(err) {
+ // See https://gitlab.com/cznic/libc/-/issues/48#note_2952938171
+ if !os.IsNotExist(err) && !os.IsPermission(err) {
panic(err)
}
@@ -732,7 +733,8 @@ func init() {
// Load services
data, err = ioutil.ReadFile("/etc/services")
if err != nil {
- if !os.IsNotExist(err) {
+ // See https://gitlab.com/cznic/libc/-/issues/48#note_2952938171
+ if !os.IsNotExist(err) && !os.IsPermission(err) {
panic(err)
}
diff --git a/vendor/modernc.org/sqlite/AUTHORS b/vendor/modernc.org/sqlite/AUTHORS
index 7b23676ace..9a3bfefc00 100644
--- a/vendor/modernc.org/sqlite/AUTHORS
+++ b/vendor/modernc.org/sqlite/AUTHORS
@@ -29,4 +29,4 @@ Saed SayedAhmed
Steffen Butzer
Toni Spets
W. Michael Petullo
-SUSE LLC
+SUSE LLC
\ No newline at end of file
diff --git a/vendor/modernc.org/sqlite/CONTRIBUTORS b/vendor/modernc.org/sqlite/CONTRIBUTORS
index e853421647..7fc3587336 100644
--- a/vendor/modernc.org/sqlite/CONTRIBUTORS
+++ b/vendor/modernc.org/sqlite/CONTRIBUTORS
@@ -10,6 +10,7 @@ Alexander Menzhinsky
Alexey Palazhchenko
Angus Dippenaar
Artyom Pervukhin
+Adrian Witas
Dan Kortschak
Dan Peterson
David Skinner
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go b/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go
index 1079f5fdd1..847d6a7e50 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_darwin_amd64.go
@@ -227735,4 +227735,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go b/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go
index 6a63273d7c..df82bb941d 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_darwin_arm64.go
@@ -227308,4 +227308,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go
index ffb049e2e9..d553a52817 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_amd64.go
@@ -221564,4 +221564,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go
index e32db26d77..238f333eda 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_freebsd_arm64.go
@@ -221577,4 +221577,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go
index 8b341113ee..ce1dd56a9a 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_386.go
@@ -221713,4 +221713,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go
index 0f3932a514..b67cafd961 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_amd64.go
@@ -221726,4 +221726,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go
index 17ccb37c3d..877ac9bea5 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm.go
@@ -222010,4 +222010,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go
index 8d7fd60f26..b1c0f9c292 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_arm64.go
@@ -221716,4 +221716,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go
index 049a348704..376d8b6a69 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_loong64.go
@@ -221815,4 +221815,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go
index 8117bdfe8f..d2012c7aa4 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_ppc64le.go
@@ -221763,4 +221763,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go
index f3a6bcac1e..5eeb2fb549 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_riscv64.go
@@ -221709,4 +221709,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go b/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go
index 9b766c8526..482192722f 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_linux_s390x.go
@@ -221663,4 +221663,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_windows.go b/vendor/modernc.org/sqlite/lib/sqlite_windows.go
index 5b3a68f1c0..2dd9f666ef 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_windows.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_windows.go
@@ -295017,4 +295017,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go b/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go
index 4cdc532ec0..0a65da134a 100644
--- a/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go
+++ b/vendor/modernc.org/sqlite/lib/sqlite_windows_386.go
@@ -294909,4 +294909,3 @@ type Sqlite3_index_info = sqlite3_index_info
type Sqlite3_module = sqlite3_module
type Sqlite3_vtab = sqlite3_vtab
type Sqlite3_vtab_cursor = sqlite3_vtab_cursor
-
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 76d3c9b1a2..c1854d5386 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -61,8 +61,8 @@ github.com/CortexFoundation/robot/backend
# github.com/CortexFoundation/statik v0.0.0-20210315012922-8bb8a7b5dc66
## explicit; go 1.16
github.com/CortexFoundation/statik
-# github.com/CortexFoundation/torrentfs v1.0.73-0.20251217130652-29bcb4ed05d5
-## explicit; go 1.24.4
+# github.com/CortexFoundation/torrentfs v1.0.73-0.20251221124821-bba7040b393f
+## explicit; go 1.24.9
github.com/CortexFoundation/torrentfs
github.com/CortexFoundation/torrentfs/backend
github.com/CortexFoundation/torrentfs/backend/caffe
@@ -81,7 +81,7 @@ github.com/Microsoft/go-winio/internal/fs
github.com/Microsoft/go-winio/internal/socket
github.com/Microsoft/go-winio/internal/stringbuffer
github.com/Microsoft/go-winio/pkg/guid
-# github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251213223233-751f36331c62
+# github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251225023818-8886bb81c549
## explicit; go 1.24.0
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime
# github.com/RoaringBitmap/roaring v1.9.4
@@ -674,7 +674,7 @@ github.com/golang/snappy
# github.com/google/btree v1.1.3
## explicit; go 1.18
github.com/google/btree
-# github.com/google/flatbuffers v25.9.23+incompatible
+# github.com/google/flatbuffers v25.12.19+incompatible
## explicit
github.com/google/flatbuffers/go
# github.com/google/go-cmp v0.7.0
@@ -752,7 +752,7 @@ github.com/influxdata/line-protocol
# github.com/jackpal/go-nat-pmp v1.0.2
## explicit
github.com/jackpal/go-nat-pmp
-# github.com/jedib0t/go-pretty/v6 v6.7.7
+# github.com/jedib0t/go-pretty/v6 v6.7.8
## explicit; go 1.18
github.com/jedib0t/go-pretty/v6/progress
github.com/jedib0t/go-pretty/v6/text
@@ -849,9 +849,13 @@ github.com/naoina/toml/ast
# github.com/ncruces/go-strftime v1.0.0
## explicit; go 1.17
github.com/ncruces/go-strftime
-# github.com/nutsdb/nutsdb v1.0.4
+# github.com/nutsdb/nutsdb v1.1.0
## explicit; go 1.18
github.com/nutsdb/nutsdb
+github.com/nutsdb/nutsdb/internal/data
+github.com/nutsdb/nutsdb/internal/fileio
+github.com/nutsdb/nutsdb/internal/testutils
+github.com/nutsdb/nutsdb/internal/utils
# github.com/oapi-codegen/runtime v1.1.2
## explicit; go 1.20
github.com/oapi-codegen/runtime
@@ -945,16 +949,16 @@ github.com/pion/randutil
# github.com/pion/rtcp v1.2.16
## explicit; go 1.21
github.com/pion/rtcp
-# github.com/pion/rtp v1.8.26
+# github.com/pion/rtp v1.8.27
## explicit; go 1.21
github.com/pion/rtp
github.com/pion/rtp/codecs
github.com/pion/rtp/codecs/av1/obu
github.com/pion/rtp/codecs/vp9
-# github.com/pion/sctp v1.8.41
+# github.com/pion/sctp v1.9.0
## explicit; go 1.21
github.com/pion/sctp
-# github.com/pion/sdp/v3 v3.0.16
+# github.com/pion/sdp/v3 v3.0.17
## explicit; go 1.21
github.com/pion/sdp/v3
# github.com/pion/srtp/v3 v3.0.9
@@ -993,7 +997,7 @@ github.com/pion/turn/v4/internal/client
github.com/pion/turn/v4/internal/ipnet
github.com/pion/turn/v4/internal/proto
github.com/pion/turn/v4/internal/server
-# github.com/pion/webrtc/v4 v4.1.8
+# github.com/pion/webrtc/v4 v4.2.1
## explicit; go 1.21
github.com/pion/webrtc/v4
github.com/pion/webrtc/v4/internal/fmtp
@@ -1132,9 +1136,6 @@ github.com/xo/terminfo
# github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342
## explicit; go 1.15
github.com/xrash/smetrics
-# github.com/xujiajun/mmap-go v1.0.1
-## explicit
-github.com/xujiajun/mmap-go
# github.com/xujiajun/utils v0.0.0-20220904132955-5f7c5b914235
## explicit; go 1.13
github.com/xujiajun/utils/filesystem
@@ -1207,7 +1208,7 @@ golang.org/x/crypto/ripemd160
golang.org/x/crypto/scrypt
golang.org/x/crypto/sha3
golang.org/x/crypto/ssh/terminal
-# golang.org/x/exp v0.0.0-20251209150349-8475f28825e9
+# golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93
## explicit; go 1.24.0
golang.org/x/exp/constraints
golang.org/x/exp/rand
@@ -1366,7 +1367,7 @@ gopkg.in/yaml.v3
lukechampine.com/blake3
lukechampine.com/blake3/bao
lukechampine.com/blake3/guts
-# modernc.org/libc v1.67.1
+# modernc.org/libc v1.67.2
## explicit; go 1.24.0
modernc.org/libc
modernc.org/libc/errno
@@ -1400,7 +1401,7 @@ modernc.org/mathutil
# modernc.org/memory v1.11.0
## explicit; go 1.23.0
modernc.org/memory
-# modernc.org/sqlite v1.40.1
+# modernc.org/sqlite v1.41.0
## explicit; go 1.24.0
modernc.org/sqlite/lib
# zombiezen.com/go/sqlite v1.4.2