diff --git a/go.mod b/go.mod index 9218edc9..78c91042 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/redhat-data-and-ai/usernaut -go 1.24.2 +go 1.25.0 require ( github.com/alicebob/miniredis/v2 v2.37.0 @@ -16,8 +16,8 @@ require ( github.com/opentracing-contrib/go-stdlib v1.1.1 github.com/opentracing/opentracing-go v1.2.0 github.com/patrickmn/go-cache v2.1.0+incompatible - github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 - github.com/redis/go-redis/v9 v9.18.0 + github.com/redis/go-redis/extra/redisotel/v9 v9.20.1 + github.com/redis/go-redis/v9 v9.20.1 github.com/sirupsen/logrus v1.9.4 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 @@ -44,7 +44,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -97,7 +96,7 @@ require ( github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.57.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect - github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 // indirect + github.com/redis/go-redis/extra/rediscmd/v9 v9.20.1 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/smartystreets/assertions v0.0.0-00010101000000-000000000000 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect @@ -114,12 +113,12 @@ require ( github.com/yuin/gopher-lua v1.1.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect - go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 // indirect - go.opentelemetry.io/otel/metric v1.41.0 // indirect - go.opentelemetry.io/otel/sdk v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.43.0 // indirect + go.opentelemetry.io/otel/sdk v1.43.0 // indirect + go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -132,7 +131,7 @@ require ( golang.org/x/mod v0.32.0 // indirect golang.org/x/net v0.50.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/term v0.40.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/time v0.12.0 // indirect diff --git a/go.sum b/go.sum index 745167e2..59bff642 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= @@ -233,12 +231,12 @@ github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3Adq github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 h1:QY4nmPHLFAJjtT5O4OMUEOxP8WVaRNOFpcbmxT2NLZU= -github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0/go.mod h1:WH8cY/0fT41Bsf341qzo8v4nx0GCE8FykAA23IVbVmo= -github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 h1:2dKdoEYBJ0CZCLPiCdvvc7luz3DPwY6hKdzjL6m1eHE= -github.com/redis/go-redis/extra/redisotel/v9 v9.18.0/go.mod h1:WzkrVG9ro9BwCQD0eJOWn6AGL4Z1CleGflM45w1hu10= -github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs= -github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0= +github.com/redis/go-redis/extra/rediscmd/v9 v9.20.1 h1:NK7vIlrbkz4xM01gzUe+SruDvAb6Brx/PdirNJPjIg4= +github.com/redis/go-redis/extra/rediscmd/v9 v9.20.1/go.mod h1:A3RExCp0qVQt36mdE4nqkFf6YmrO6Mt2BgJeIOVOGVc= +github.com/redis/go-redis/extra/redisotel/v9 v9.20.1 h1:x1zIEhw3jvBHPcPrXLfrp3pGHLHAX4er6sjw7GsbkZk= +github.com/redis/go-redis/extra/redisotel/v9 v9.20.1/go.mod h1:NDgRP4yN02H1aVGDLvaCiRN+CuvA/ELP2dDLnyLKyUM= +github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w= +github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -297,28 +295,28 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= -github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= +github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= gitlab.com/gitlab-org/api/client-go v0.145.0 h1:gvi4bwoF6fyQq6kJix4WicApy/jBRpGlqzI0PDRD9kU= gitlab.com/gitlab-org/api/client-go v0.145.0/go.mod h1:eABRp++g3IbUP10ZeBIys+9g59dgJnlQLEk8XgKNB54= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= -go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= -go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= -go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= -go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= @@ -371,8 +369,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= diff --git a/vendor/github.com/dgryski/go-rendezvous/LICENSE b/vendor/github.com/dgryski/go-rendezvous/LICENSE deleted file mode 100644 index 22080f73..00000000 --- a/vendor/github.com/dgryski/go-rendezvous/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017-2020 Damian Gryski - -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/github.com/dgryski/go-rendezvous/rdv.go b/vendor/github.com/dgryski/go-rendezvous/rdv.go deleted file mode 100644 index 7a6f8203..00000000 --- a/vendor/github.com/dgryski/go-rendezvous/rdv.go +++ /dev/null @@ -1,79 +0,0 @@ -package rendezvous - -type Rendezvous struct { - nodes map[string]int - nstr []string - nhash []uint64 - hash Hasher -} - -type Hasher func(s string) uint64 - -func New(nodes []string, hash Hasher) *Rendezvous { - r := &Rendezvous{ - nodes: make(map[string]int, len(nodes)), - nstr: make([]string, len(nodes)), - nhash: make([]uint64, len(nodes)), - hash: hash, - } - - for i, n := range nodes { - r.nodes[n] = i - r.nstr[i] = n - r.nhash[i] = hash(n) - } - - return r -} - -func (r *Rendezvous) Lookup(k string) string { - // short-circuit if we're empty - if len(r.nodes) == 0 { - return "" - } - - khash := r.hash(k) - - var midx int - var mhash = xorshiftMult64(khash ^ r.nhash[0]) - - for i, nhash := range r.nhash[1:] { - if h := xorshiftMult64(khash ^ nhash); h > mhash { - midx = i + 1 - mhash = h - } - } - - return r.nstr[midx] -} - -func (r *Rendezvous) Add(node string) { - r.nodes[node] = len(r.nstr) - r.nstr = append(r.nstr, node) - r.nhash = append(r.nhash, r.hash(node)) -} - -func (r *Rendezvous) Remove(node string) { - // find index of node to remove - nidx := r.nodes[node] - - // remove from the slices - l := len(r.nstr) - r.nstr[nidx] = r.nstr[l] - r.nstr = r.nstr[:l] - - r.nhash[nidx] = r.nhash[l] - r.nhash = r.nhash[:l] - - // update the map - delete(r.nodes, node) - moved := r.nstr[nidx] - r.nodes[moved] = nidx -} - -func xorshiftMult64(x uint64) uint64 { - x ^= x >> 12 // a - x ^= x << 25 // b - x ^= x >> 27 // c - return x * 2685821657736338717 -} diff --git a/vendor/github.com/redis/go-redis/extra/redisotel/v9/config.go b/vendor/github.com/redis/go-redis/extra/redisotel/v9/config.go index b9311bea..2f7d0be2 100644 --- a/vendor/github.com/redis/go-redis/extra/redisotel/v9/config.go +++ b/vendor/github.com/redis/go-redis/extra/redisotel/v9/config.go @@ -100,6 +100,12 @@ func WithAttributes(attrs ...attribute.KeyValue) Option { }) } +func WithPoolName(poolName string) Option { + return option(func(conf *config) { + conf.poolName = poolName + }) +} + //------------------------------------------------------------------------------ type TracingOption interface { diff --git a/vendor/github.com/redis/go-redis/extra/redisotel/v9/metrics.go b/vendor/github.com/redis/go-redis/extra/redisotel/v9/metrics.go index a6630ee0..031f1ccf 100644 --- a/vendor/github.com/redis/go-redis/extra/redisotel/v9/metrics.go +++ b/vendor/github.com/redis/go-redis/extra/redisotel/v9/metrics.go @@ -37,6 +37,25 @@ func InstrumentMetrics(rdb redis.UniversalClient, opts ...MetricsOption) error { metric.WithInstrumentationVersion("semver:"+redis.Version()), ) } + if conf.poolName == "" { + switch rdb := rdb.(type) { + case *redis.Client: + conf.poolName = rdb.Options().Addr + case *redis.ClusterClient: + for _, addr := range rdb.Options().Addrs { + conf.poolName = addr + break + } + case *redis.Ring: + for _, addr := range rdb.Options().Addrs { + conf.poolName = addr + break + } + default: + return fmt.Errorf("redisotel: %T not supported", rdb) + } + } + conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName)) var state *metricsState if conf.closeChan != nil { @@ -93,12 +112,6 @@ func registerClient(rdb *redis.Client, conf *config, state *metricsState) error } } - if conf.poolName == "" { - opt := rdb.Options() - conf.poolName = opt.Addr - } - conf.attrs = append(conf.attrs, attribute.String("pool.name", conf.poolName)) - registration, err := reportPoolStats(rdb, conf) if err != nil { return err diff --git a/vendor/github.com/redis/go-redis/v9/Makefile b/vendor/github.com/redis/go-redis/v9/Makefile index 370f3880..90f03b57 100644 --- a/vendor/github.com/redis/go-redis/v9/Makefile +++ b/vendor/github.com/redis/go-redis/v9/Makefile @@ -1,8 +1,8 @@ GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) -REDIS_VERSION ?= 8.6 +REDIS_VERSION ?= 8.8 RE_CLUSTER ?= false RCE_DOCKER ?= true -CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:custom-21860421418-debian-amd64 +CLIENT_LIBS_TEST_IMAGE ?= redislabs/client-libs-test:8.8.0 docker.start: export RE_CLUSTER=$(RE_CLUSTER) && \ @@ -49,7 +49,7 @@ test.ci: export RE_CLUSTER=$(RE_CLUSTER) && \ export RCE_DOCKER=$(RCE_DOCKER) && \ export REDIS_VERSION=$(REDIS_VERSION) && \ - go mod tidy -compat=1.18 && \ + go mod tidy && \ go vet && \ go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race -skip Example); \ done @@ -63,7 +63,7 @@ test.ci.skip-vectorsets: export RE_CLUSTER=$(RE_CLUSTER) && \ export RCE_DOCKER=$(RCE_DOCKER) && \ export REDIS_VERSION=$(REDIS_VERSION) && \ - go mod tidy -compat=1.18 && \ + go mod tidy && \ go vet && \ go test -v -coverprofile=coverage.txt -covermode=atomic ./... -race \ -run '^(?!.*(?:VectorSet|vectorset|ExampleClient_vectorset)).*$$' -skip Example); \ @@ -118,5 +118,5 @@ go_mod_tidy: echo "go mod tidy in $${dir}"; \ (cd "$${dir}" && \ go get -u ./... && \ - go mod tidy -compat=1.18); \ + go mod tidy); \ done diff --git a/vendor/github.com/redis/go-redis/v9/README.md b/vendor/github.com/redis/go-redis/v9/README.md index 160714ab..ae90d2b7 100644 --- a/vendor/github.com/redis/go-redis/v9/README.md +++ b/vendor/github.com/redis/go-redis/v9/README.md @@ -18,16 +18,24 @@ In `go-redis` we are aiming to support the last three releases of Redis. Currently, this means we do support: - [Redis 8.0](https://raw.githubusercontent.com/redis/redis/8.0/00-RELEASENOTES) - using Redis CE 8.0 -- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2 +- [Redis 8.2](https://raw.githubusercontent.com/redis/redis/8.2/00-RELEASENOTES) - using Redis CE 8.2 - [Redis 8.4](https://raw.githubusercontent.com/redis/redis/8.4/00-RELEASENOTES) - using Redis CE 8.4 +- [Redis 8.8](https://raw.githubusercontent.com/redis/redis/8.8/00-RELEASENOTES) - using Redis CE 8.8 -Although the `go.mod` states it requires at minimum `go 1.21`, our CI is configured to run the tests against all three -versions of Redis and multiple versions of Go ([1.21](https://go.dev/doc/devel/release#go1.21.0), -[1.23](https://go.dev/doc/devel/release#go1.23.0), oldstable, and stable). We observe that some modules related test may not pass with +Although the `go.mod` states it requires at minimum `go 1.24`, our CI is configured to run the tests against all supported +versions of Redis and multiple versions of Go ([1.24](https://go.dev/doc/devel/release#go1.24.0), oldstable, and stable). We observe that some modules related test may not pass with Redis Stack 7.2 and some commands are changed with Redis CE 8.0. Although it is not officially supported, `go-redis/v9` should be able to work with any Redis 7.0+. Please do refer to the documentation and the tests if you experience any issues. +### Array data type (Redis 8.8+) + +Starting with Redis 8.8, go-redis exposes the new array data type via the `AR*` command family +(`ARSET`, `ARGET`, `ARGETRANGE`, `ARMSET`, `ARMGET`, `ARINSERT`, `ARDEL`, `ARDELRANGE`, +`ARLEN`, `ARCOUNT`, `ARNEXT`, `ARSEEK`, `ARSCAN`, `ARGREP`, `ARRING`, `ARLASTITEMS`, +`ARINFO`/`ARINFOFULL`, and the `AROP*` reducers). See `array_commands.go` for the full +surface. The API is experimental and may change in a future release. + ## How do I Redis? [Learn for free at Redis University](https://university.redis.com/) @@ -49,6 +57,7 @@ Please do refer to the documentation and the tests if you experience any issues. - [Chat](https://discord.gg/W4txy5AeKM) - [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9) - [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples) +- [Release notes](./RELEASE-NOTES.md) ([GitHub Releases](https://github.com/redis/go-redis/releases)) ## old documentation @@ -136,6 +145,29 @@ func ExampleClient() { } ``` +### Dial retries and backoff + +Connection establishment can be retried by the connection pool when dialing fails. + +- **`DialerRetries`**: maximum number of dial attempts (default: 5). +- **`DialerRetryTimeout`**: default delay between attempts when no custom backoff is provided (default: 100ms). +- **`DialerRetryBackoff`**: optional function hook to control the delay between attempts. + +Example: + +```go +rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + + DialerRetries: 5, + DialerRetryTimeout: 100 * time.Millisecond, // used when DialerRetryBackoff is nil + + // Optional: exponential backoff with jitter and a cap. + DialerRetryBackoff: redis.DialRetryBackoffExponential(100*time.Millisecond, 2*time.Second), +}) +defer rdb.Close() +``` + ### Authentication The Redis client supports multiple ways to provide authentication credentials, with a clear priority order. Here are the available options: @@ -333,18 +365,17 @@ rdb := redis.NewClient(&redis.Options{ }) ``` -#### Unstable RESP3 Structures for RediSearch Commands -When integrating Redis with application functionalities using RESP3, it's important to note that some response structures aren't final yet. This is especially true for more complex structures like search and query results. We recommend using RESP2 when using the search and query capabilities, but we plan to stabilize the RESP3-based API-s in the coming versions. You can find more guidance in the upcoming release notes. +#### RESP3 for RediSearch Commands (`UnstableResp3` is deprecated) +As of v9.20, `FT.SEARCH`, `FT.AGGREGATE`, `FT.INFO`, `FT.SPELLCHECK`, and `FT.SYNDUMP` +parse RESP3 (map) responses into the same typed result objects as RESP2. **No flag +is required β€” `Val()` / `Result()` work uniformly on both protocols.** -To enable unstable RESP3, set the option in your client configuration: +The legacy `UnstableResp3` option is now a **no-op** and is retained on every +options struct only for backwards compatibility. It will be removed in a future +release; new code should not set it. -```go -redis.NewClient(&redis.Options{ - UnstableResp3: true, - }) -``` -**Note:** When UnstableResp3 mode is enabled, it's necessary to use RawResult() and RawVal() to retrieve a raw data. - Since, raw response is the only option for unstable search commands Val() and Result() calls wouldn't have any affect on them: +`RawResult()` / `RawVal()` continue to work for callers that prefer the raw RESP +payload directly: ```go res1, err := client.FTSearchWithArgs(ctx, "txt", "foo bar", &redis.FTSearchOptions{}).RawResult() diff --git a/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md b/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md index 7b705ee6..de24456c 100644 --- a/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md +++ b/vendor/github.com/redis/go-redis/v9/RELEASE-NOTES.md @@ -1,5 +1,209 @@ # Release Notes +# 9.20.1 (2026-06-11) + +This is a patch release containing bug fixes only. There are no new features or breaking changes; upgrading from 9.20.0 is a drop-in replacement. + +## πŸš€ Highlights + +### RESP3 pub/sub message loss fixed + +`PeekPushNotificationName` previously inspected only the bytes already buffered by `bufio`, so when a push frame header straddled a buffer fill boundary it could return a **truncated** notification name (e.g. `"messa"` instead of `"message"`). The push processor then mis-routed the frame and `ReadReply` silently dropped it, causing intermittent RESP3 pub/sub message loss. The peek now grows its window (36 bytes β†’ up to 4 KiB) and reads more from the connection until the header is complete, cleanly separating incomplete prefixes from corrupt frames (including overflow-safe bulk-length handling). Fixes [#3839](https://github.com/redis/go-redis/issues/3839). + +([#3842](https://github.com/redis/go-redis/pull/3842)) by [@ndyakov](https://github.com/ndyakov) + +## πŸ› Bug Fixes + +- **RESP3 push peeking**: `PeekPushNotificationName` no longer returns a truncated notification name when a push frame header spans a buffer boundary, preventing silent RESP3 pub/sub message loss (fixes [#3839](https://github.com/redis/go-redis/issues/3839)) ([#3842](https://github.com/redis/go-redis/pull/3842)) by [@ndyakov](https://github.com/ndyakov) +- **`FT.HYBRID` vector params**: Vector data is now always sent via `PARAMS` with auto-generated param names (`__vector_param_N`, with collision avoidance) when `VectorParamName` is omitted, since Redis no longer accepts inline vector blobs; the `FTHybridOptions.Params` map is no longer mutated, so the same options struct can be reused across calls ([#3844](https://github.com/redis/go-redis/pull/3844)) by [@ndyakov](https://github.com/ndyakov) +- **`CLUSTER SHARDS` forward compatibility**: Unknown shard- and node-level attributes in the `CLUSTER SHARDS` reply are now skipped via `DiscardNext()` instead of erroring, so clients keep working when the server introduces new fields ([#3843](https://github.com/redis/go-redis/pull/3843)) by [@madolson](https://github.com/madolson) +- **PubSub double reconnect**: `PubSub.releaseConn` no longer reconnects twice when a connection is both unusable (or pending handoff) and reports a bad-connection error, avoiding a wasted connection establish-then-close cycle ([#3833](https://github.com/redis/go-redis/pull/3833)) by [@cxljs](https://github.com/cxljs) + +## πŸ‘₯ Contributors + +We'd like to thank all the contributors who worked on this release! + +[@cxljs](https://github.com/cxljs), [@madolson](https://github.com/madolson), [@ndyakov](https://github.com/ndyakov) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.20.0...v9.20.1 + +# 9.20.0 (2026-05-28) + +## πŸš€ Highlights + +### Redis 8.8 Support + +This release adds support for **Redis 8.8**. The README's supported-versions list now includes Redis 8.8 alongside 8.0/8.2/8.4, and CI exercises the `8.8-rc1` client-libs-test image across the full suite (Makefile, build workflow, doctests, run-tests action, and docker-compose). + +Coverage for the new commands that ship in the 8.x line, rounded out in this release: + +- **`AR*` array data type** ([#3813](https://github.com/redis/go-redis/pull/3813)) β€” new array data structure, exposed via the `ArrayCmdable` interface (see the experimental-features highlight below). +- **`INCREX`** ([#3816](https://github.com/redis/go-redis/pull/3816)) β€” atomic increment with expiration in a single round-trip. +- **`XNACK`** ([#3790](https://github.com/redis/go-redis/pull/3790)) β€” explicit negative-acknowledge of pending stream entries. +- **`XAUTOCLAIM` PEL deletes** ([#3798](https://github.com/redis/go-redis/pull/3798)) β€” `XAUTOCLAIM`/`XAUTOCLAIMJUSTID` now return the list of deleted message IDs from the pending entries list. +- **`TS.RANGE` multiple aggregators** ([#3791](https://github.com/redis/go-redis/pull/3791)) β€” `TS.RANGE`/`TS.REVRANGE`/`TS.MRANGE`/`TS.MREVRANGE` accept multiple aggregators in a single call. +- **`Z(UNION|INTER|DIFF)` `COUNT` aggregator** ([#3802](https://github.com/redis/go-redis/pull/3802)) β€” `COUNT` reducer for sorted-set set operations. +- **`JSON.SET FPHA`** ([#3797](https://github.com/redis/go-redis/pull/3797)) β€” new `FPHA` argument that specifies the floating-point type for homogeneous FP arrays. + +CI image bump ([#3814](https://github.com/redis/go-redis/pull/3814)) by [@ofekshenawa](https://github.com/ofekshenawa). Command coverage contributions by [@cxljs](https://github.com/cxljs), [@elena-kolevska](https://github.com/elena-kolevska), [@Khukharr](https://github.com/Khukharr), [@ndyakov](https://github.com/ndyakov), and [@ofekshenawa](https://github.com/ofekshenawa). + +### Stable RESP3 for RediSearch (`UnstableResp3` deprecated) + +`FT.SEARCH`, `FT.AGGREGATE`, `FT.INFO`, `FT.SPELLCHECK`, and `FT.SYNDUMP` now parse RESP3 (map) responses into the same typed result objects as RESP2 β€” `Val()` and `Result()` work uniformly on both protocols, no flag required. Previously, RESP3 search responses required `UnstableResp3: true` and were returned as opaque maps accessible only via `RawResult()` / `RawVal()`. + +As a result, the `UnstableResp3` option is now a **no-op** across every options struct (`Options`, `ClusterOptions`, `UniversalOptions`, `FailoverOptions`, `RingOptions`) and has been marked `// Deprecated:`. The field is retained for backwards compatibility β€” existing code that sets `UnstableResp3: true` will continue to compile and behave identically β€” but it will be removed in a future release and new code should not set it. `RawResult()` / `RawVal()` continue to work for callers that prefer the raw RESP payload. + +([#3741](https://github.com/redis/go-redis/pull/3741)) by [@ndyakov](https://github.com/ndyakov) + +### Experimental Array Data Structure Commands + +Adds an experimental `ArrayCmdable` interface with the `AR*` command family (`ARSet`, `ARGet`, `ARGetRange`, `ARMSet`, `ARMGet`, `ARDel`, `ARDelRange`, `ARScan`, `ARSeek`, `ARNext`, `ARLastItems`, `ARGrep`, `ARGrepWithValues`, `ARInfo`/`ARInfoFull`, and typed reducers `AROpSum`/`AROpMin`/`AROpMax`/`AROpAnd`/`AROpOr`/`AROpXor`/`AROpMatch`/`AROpUsed`) for working with Redis 8.8's new array data type. **API is experimental and may change in a future release.** + +([#3813](https://github.com/redis/go-redis/pull/3813)) by [@cxljs](https://github.com/cxljs) + +## ✨ New Features + +- **RESP3 search parser**: First-class RESP3 parsing for `FT.SEARCH`/`FT.AGGREGATE`/`FT.INFO`/`FT.SPELLCHECK`/`FT.SYNDUMP` responses with backwards compatibility for RESP2 ([#3741](https://github.com/redis/go-redis/pull/3741)) by [@ndyakov](https://github.com/ndyakov) +- **INCREX**: New `INCREX` command support β€” atomic increment with expiration ([#3816](https://github.com/redis/go-redis/pull/3816)) by [@ndyakov](https://github.com/ndyakov) +- **XNACK**: Client support for the `XNACK` stream command for explicitly negative-acknowledging pending entries ([#3790](https://github.com/redis/go-redis/pull/3790)) by [@elena-kolevska](https://github.com/elena-kolevska) +- **TS range multiple aggregators**: `TS.RANGE`/`TS.REVRANGE`/`TS.MRANGE`/`TS.MREVRANGE` now accept multiple aggregators in a single call ([#3791](https://github.com/redis/go-redis/pull/3791)) by [@elena-kolevska](https://github.com/elena-kolevska) +- **`XAutoClaim` deleted IDs**: `XAUTOCLAIM`/`XAUTOCLAIMJUSTID` now return the list of deleted message IDs from the PEL ([#3798](https://github.com/redis/go-redis/pull/3798)) by [@Khukharr](https://github.com/Khukharr) +- **`JSON.SET FPHA`**: `JSON.SET` accepts a new `FPHA` argument that specifies the floating-point type for homogeneous floating-point arrays ([#3797](https://github.com/redis/go-redis/pull/3797)) by [@ndyakov](https://github.com/ndyakov) +- **Sorted-set union/intersection COUNT**: `ZUNION`/`ZINTER`/`ZDIFF` aggregator now supports `COUNT` ([#3802](https://github.com/redis/go-redis/pull/3802)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **`FT.HYBRID` vector validation**: Validates hybrid-search vector input types and adds proper typed vector parameters ([#3756](https://github.com/redis/go-redis/pull/3756)) by [@DengY11](https://github.com/DengY11) +- **Cluster pool wait stats**: `ClusterClient.PoolStats()` now accumulates `WaitCount` and `WaitDurationNs` across all node pools (previously always zero) ([#3809](https://github.com/redis/go-redis/pull/3809)) by [@LINKIWI](https://github.com/LINKIWI) + +## πŸ› Bug Fixes + +- **TLS-only Cluster PubSub**: `CLUSTER SLOTS` port-0 entries now fall back to the origin endpoint's port, fixing `dial tcp :0: connection refused` on TLS-only clusters started with `--port 0 --tls-port ` (fixes [#3726](https://github.com/redis/go-redis/issues/3726)) ([#3828](https://github.com/redis/go-redis/pull/3828)) by [@ndyakov](https://github.com/ndyakov) +- **Sharded PubSub reconnect routing**: `PubSub.conn()` now passes both regular (`c.channels`) and sharded (`c.schannels`) channels into the per-PubSub `newConn` closure. Previously, `ClusterClient.SSubscribe`-only PubSubs reconnected to a random node (because the routing closure saw an empty channel list), the `SSUBSCRIBE` was sent to the wrong shard, and the resulting `MOVED` reply was silently dropped ([#3829](https://github.com/redis/go-redis/pull/3829)) by [@ndyakov](https://github.com/ndyakov) +- **ClusterClient `Watch` retry**: User errors returned from a `Watch` callback are no longer subjected to cluster-retry classification; transient cluster errors still retry, but a callback returning e.g. `net.ErrClosed` short-circuits immediately ([#3821](https://github.com/redis/go-redis/pull/3821)) by [@obiyang](https://github.com/obiyang) +- **Sentinel concurrent-probe leak**: `MasterAddr`'s concurrent sentinel probe now closes the non-winning sentinel clients instead of leaking them ([#3827](https://github.com/redis/go-redis/pull/3827)) by [@cxljs](https://github.com/cxljs) +- **Sentinel rediscovery loop on master-only setups**: `replicaAddrs` no longer tears down the cached sentinel client when the replica list is empty, eliminating a continuous rediscovery loop on master-only Sentinel deployments that flooded logs and added per-operation latency ([#3795](https://github.com/redis/go-redis/pull/3795)) by [@shahyash2609](https://github.com/shahyash2609) +- **Pool `CloseConn` hooks**: `Pool.CloseConn` now triggers registered hooks, fixing a memory leak when connections are closed explicitly rather than via the normal removal path ([#3818](https://github.com/redis/go-redis/pull/3818)) by [@ndyakov](https://github.com/ndyakov) +- **Dial TCP error redirection**: Wrapped `dial tcp` errors are now correctly classified as redirectable so cluster routing can recover from a single unreachable node ([#3810](https://github.com/redis/go-redis/pull/3810)) by [@vladisa88](https://github.com/vladisa88) +- **Pool `Close` health checks**: `ConnPool.Close` now only runs health checks against idle connections, avoiding spurious activity on connections still in use ([#3805](https://github.com/redis/go-redis/pull/3805)) by [@ndyakov](https://github.com/ndyakov) +- **VLinks return type**: Fixed the return type of `VLINKS`/`VLINKSWITHSCORES` vector-set replies ([#3820](https://github.com/redis/go-redis/pull/3820)) by [@romanpovol](https://github.com/romanpovol) + +## πŸ§ͺ Testing & Infrastructure + +- **Flaky tests**: Stabilized several flaky tests in the sentinel and pool suites ([#3815](https://github.com/redis/go-redis/pull/3815)) by [@ndyakov](https://github.com/ndyakov) +- **Sentinel failover metric race**: Fixed a data race in the sentinel failover metric test ([#3824](https://github.com/redis/go-redis/pull/3824)) by [@cxljs](https://github.com/cxljs) +- **`waitForSentinelClusterStable` post-conditions**: The sentinel test harness now waits for replicas to be fully connected (not just present in the count) and is robust to randomized spec ordering after failover specs, eliminating an intermittent `Expected master to equal slave` flake ([#3830](https://github.com/redis/go-redis/pull/3830)) by [@ndyakov](https://github.com/ndyakov) +- **`govulncheck` workflow**: New scheduled GitHub Actions workflow runs `govulncheck` on every push, PR, and weekly, surfacing newly disclosed Go vulnerabilities even when no code changes ([#3779](https://github.com/redis/go-redis/pull/3779)) by [@solardome](https://github.com/solardome) +- **CI Redis 8.8-rc1**: CI now exercises the 8.8-rc1 Redis image ([#3814](https://github.com/redis/go-redis/pull/3814)) by [@ofekshenawa](https://github.com/ofekshenawa) + +## 🧰 Maintenance + +- **`Cmd.Slot()` lookup refactor**: Caches the per-command `CommandInfo` and short-circuits keyless commands before the switch dispatch, removing redundant `Peek` calls ([#3804](https://github.com/redis/go-redis/pull/3804)) by [@retr0-kernel](https://github.com/retr0-kernel) +- **stdlib `math/rand`**: Replaced `internal/rand` with `math/rand` from the standard library now that the minimum Go version is 1.24 ([#3823](https://github.com/redis/go-redis/pull/3823)) by [@cxljs](https://github.com/cxljs) +- **ConnPool queue channel**: Removed the unused queue channel from `ConnPool`, trimming the pool's footprint ([#3826](https://github.com/redis/go-redis/pull/3826)) by [@cxljs](https://github.com/cxljs) +- **Extra packages LICENSE**: Added a LICENSE file to each `extra/*` package ([#3817](https://github.com/redis/go-redis/pull/3817)) by [@ndyakov](https://github.com/ndyakov) +- **README & CI image**: Documentation refresh and bumped the default CI image tag ([#3822](https://github.com/redis/go-redis/pull/3822)) by [@ndyakov](https://github.com/ndyakov) + +## πŸ‘₯ Contributors + +We'd like to thank all the contributors who worked on this release! + +[@cxljs](https://github.com/cxljs), [@DengY11](https://github.com/DengY11), [@elena-kolevska](https://github.com/elena-kolevska), [@Khukharr](https://github.com/Khukharr), [@LINKIWI](https://github.com/LINKIWI), [@ndyakov](https://github.com/ndyakov), [@obiyang](https://github.com/obiyang), [@ofekshenawa](https://github.com/ofekshenawa), [@retr0-kernel](https://github.com/retr0-kernel), [@romanpovol](https://github.com/romanpovol), [@shahyash2609](https://github.com/shahyash2609), [@solardome](https://github.com/solardome), [@vladisa88](https://github.com/vladisa88) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.19.0...v9.20.0 + +# 9.19.0 (2026-04-27) + +## πŸš€ Highlights + +### FIPS-Compatible Script Helper + +`Script` now supports a FIPS-safe execution mode that avoids client-side SHA-1 computation, which is blocked in strict FIPS environments. A new `NewScriptServerSHA` constructor uses `SCRIPT LOAD` to obtain and cache the digest from the server, then runs commands via `EVALSHA`/`EVALSHA_RO`. Falls back to `EVAL`/`EVALRO` if loading fails, and transparently retries once on `NOSCRIPT`. The default behavior is unchanged for existing users. + +([#3700](https://github.com/redis/go-redis/pull/3700)) by [@chaitanyabodlapati](https://github.com/chaitanyabodlapati) + +### FT.AGGREGATE Step-Based Pipeline Builder + +Added a new step-based `FT.AGGREGATE` pipeline API via `FTAggregateOptions.Steps`, allowing `LOAD`, `APPLY`, `GROUPBY`, and `SORTBY` (with per-step `MAX`) to be repeated and interleaved in arbitrary order β€” matching Redis's native multi-stage aggregation semantics. The legacy `Load`/`Apply`/`GroupBy`/`SortBy`/`SortByMax` fields are now deprecated. + +([#3782](https://github.com/redis/go-redis/pull/3782)) by [@ndyakov](https://github.com/ndyakov) + +### Raw RESP Protocol Access + +Added `DoRaw` and `DoRawWriteTo` methods for executing arbitrary commands and reading the raw RESP response. Useful for proxying, custom protocol inspection, and working with commands not yet wrapped by go-redis. + +([#3713](https://github.com/redis/go-redis/pull/3713)) by [@ofekshenawa](https://github.com/ofekshenawa) + +### Configurable Dial Retry Backoff + +Added `DialerRetryBackoff` option (plumbed through `Options`, `ClusterOptions`, `RingOptions`, `FailoverOptions`) to let callers customize the delay between failed dial attempts. Helpers `DialRetryBackoffConstant` and `DialRetryBackoffExponential` (with jitter and cap) are provided out of the box. Dial timeout is now also applied **per attempt** rather than across all retries. + +([#3706](https://github.com/redis/go-redis/pull/3706), [#3705](https://github.com/redis/go-redis/pull/3705)) by [@mwhooker](https://github.com/mwhooker) + +## ✨ New Features + +- **FT.AGGREGATE Steps**: Step-based pipeline builder for `FT.AGGREGATE` with support for repeated/interleaved `LOAD`, `APPLY`, `GROUPBY`, and `SORTBY` stages ([#3782](https://github.com/redis/go-redis/pull/3782)) by [@ndyakov](https://github.com/ndyakov) +- **VectorSet commands**: Added `VISMEMBER` and `WITHATTRIBS` support ([#3753](https://github.com/redis/go-redis/pull/3753)) by [@romanpovol](https://github.com/romanpovol) +- **FIPS-safe Script**: `NewScriptServerSHA` uses `SCRIPT LOAD` to obtain the digest from the server, avoiding client-side SHA-1 ([#3700](https://github.com/redis/go-redis/pull/3700)) by [@chaitanyabodlapati](https://github.com/chaitanyabodlapati) +- **Raw RESP access**: `DoRaw` and `DoRawWriteTo` for raw RESP protocol access ([#3713](https://github.com/redis/go-redis/pull/3713)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **Dial retry backoff**: `DialerRetryBackoff` function option with constant and exponential helpers ([#3706](https://github.com/redis/go-redis/pull/3706)) by [@mwhooker](https://github.com/mwhooker) +- **Typed NOSCRIPT error**: Redis `NOSCRIPT` replies are now surfaced as a typed error for easier handling ([#3738](https://github.com/redis/go-redis/pull/3738)) by [@LINKIWI](https://github.com/LINKIWI) +- **PubSub ClientSetName**: Added `ClientSetName` method to `PubSub` ([#3727](https://github.com/redis/go-redis/pull/3727)) by [@Flack74](https://github.com/Flack74) +- **ReplicaOf**: New `ReplicaOf` method replaces the deprecated `SlaveOf` ([#3720](https://github.com/redis/go-redis/pull/3720)) by [@Copilot](https://github.com/apps/copilot-swe-agent) +- **HSCAN BinaryUnmarshaler**: `HScan` now supports types implementing `encoding.BinaryUnmarshaler` ([#3768](https://github.com/redis/go-redis/pull/3768)) by [@Aaditya-dubey1](https://github.com/Aaditya-dubey1) + +## πŸ› Bug Fixes + +- **Auto hostname type detection**: Improved endpoint type detection for maintenance notifications using DNS-based classification; handles empty hosts and expanded private-IP ranges ([#3789](https://github.com/redis/go-redis/pull/3789)) by [@ndyakov](https://github.com/ndyakov) +- **HELLO fallback**: Don't send `CLIENT MAINT_NOTIFICATIONS` handshake when `HELLO` fails and connection falls back to RESP2; fail fast when explicitly enabled with RESP3 ([#3788](https://github.com/redis/go-redis/pull/3788)) by [@ndyakov](https://github.com/ndyakov) +- **Dial TCP retry**: `ShouldRetry` now treats `net.OpError` with `Op == "dial"` timeout errors as safe to retry since no command was sent ([#3787](https://github.com/redis/go-redis/pull/3787)) by [@vladisa88](https://github.com/vladisa88) +- **wrappedOnClose leak**: Fixed resource leak caused by repeatedly wrapping `baseClient` close logic; replaced with a bounded, concurrency-safe named-hook registry ([#3785](https://github.com/redis/go-redis/pull/3785)) by [@ndyakov](https://github.com/ndyakov) +- **Pool Close() on stale connections**: Suppress close errors (e.g., TLS `closeNotify` timeouts) for connections already dropped by the server due to idle timeout ([#3778](https://github.com/redis/go-redis/pull/3778)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **FIFO waiter ordering**: Fixed race in `ConnStateMachine.notifyWaiters` that could wake multiple waiters under a single mutex hold and violate FIFO ordering ([#3777](https://github.com/redis/go-redis/pull/3777)) by [@0x48core](https://github.com/0x48core) +- **Lua READONLY detection**: Detect `READONLY` errors embedded in Lua script error messages on read-only replicas so commands are correctly retried ([#3769](https://github.com/redis/go-redis/pull/3769)) by [@zhengjilei](https://github.com/zhengjilei) +- **VectorScoreSliceCmd RESP2**: Fixed `VSimWithScores`, `VSimWithArgsWithScores`, and `VLinksWithScores` which were broken on RESP2 connections returning flat arrays instead of maps ([#3767](https://github.com/redis/go-redis/pull/3767)) by [@Copilot](https://github.com/apps/copilot-swe-agent) +- **Closed connection handling**: Two fixes for closed connection handling in the pool ([#3764](https://github.com/redis/go-redis/pull/3764)) by [@cxljs](https://github.com/cxljs) +- **ZRangeArgs Rev**: Fixed `ZRangeArgs` with `Rev` + `ByScore`/`ByLex` incorrectly swapping `Start`/`Stop`, breaking `ZRANGESTORE` ([#3751](https://github.com/redis/go-redis/pull/3751)) by [@Copilot](https://github.com/apps/copilot-swe-agent) +- **OTel metric instrument types**: Fixed metric instrument types in `redisotel-native` ([#3743](https://github.com/redis/go-redis/pull/3743)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **Options.clone() data race**: Fixed data race when cloning `Options` ([#3739](https://github.com/redis/go-redis/pull/3739)) by [@rubensayshi](https://github.com/rubensayshi) +- **Connection closure metrics**: Fixed connection closure metrics and enabled all metric groups by default in `redisotel-native` ([#3735](https://github.com/redis/go-redis/pull/3735)) by [@ofekshenawa](https://github.com/ofekshenawa) +- **OTel semconv v1.38.0**: Use metric definition from `otel/semconv/v1.38.0` in `redisotel-native` ([#3731](https://github.com/redis/go-redis/pull/3731)) by [@wzy9607](https://github.com/wzy9607) +- **SETNX semantics**: Use `SET ... NX` instead of the deprecated `SETNX` command ([#3723](https://github.com/redis/go-redis/pull/3723)) by [@ndyakov](https://github.com/ndyakov) +- **TIME keyless routing**: Mark `TIME` as a keyless command for correct cluster routing ([#3722](https://github.com/redis/go-redis/pull/3722)) by [@fatal10110](https://github.com/fatal10110) +- **Dial timeout per retry**: Dial timeout now applies per attempt instead of across all retry attempts combined ([#3705](https://github.com/redis/go-redis/pull/3705)) by [@mwhooker](https://github.com/mwhooker) +- **Cluster metrics attributes**: Fixed `pool.name` being appended per node, which corrupted and dropped user-provided custom attributes ([#3699](https://github.com/redis/go-redis/pull/3699)) by [@Jesse-Bonfire](https://github.com/Jesse-Bonfire) +- **initConn nil dereference**: Fixed nil pointer dereference and potential deadlock in `*baseClient.initConn()`; added explicit nil option guards to client constructors ([#3676](https://github.com/redis/go-redis/pull/3676)) by [@olde-ducke](https://github.com/olde-ducke) + +## ⚑ Performance + +- **RESP reader**: Optimized RESP reader by eliminating intermediate string allocations ([#3774](https://github.com/redis/go-redis/pull/3774)) by [@Aaditya-dubey1](https://github.com/Aaditya-dubey1) +- **Inline rendezvous hashing**: Replaced `github.com/dgryski/go-rendezvous` dependency with an in-repo implementation in `internal/hashtag`, reducing the dependency graph while preserving algorithm parity ([#3762](https://github.com/redis/go-redis/pull/3762)) by [@bigsk05](https://github.com/bigsk05) + +## πŸ§ͺ Testing & Infrastructure + +- **Release automation**: Added `repository`, `ref`, and `client-libs-test-image-tag` inputs to the `run-tests` composite action; `redis-version` is now optional so unstable builds use `REDIS_VERSION` from the Makefile ([#3749](https://github.com/redis/go-redis/pull/3749)) by [@dariaguy](https://github.com/dariaguy) +- **Go 1.24**: Updated minimum Go version to 1.24 and use `-compat=1.24` in release scripts ([#3714](https://github.com/redis/go-redis/pull/3714), [#3754](https://github.com/redis/go-redis/pull/3754)) by [@ndyakov](https://github.com/ndyakov), [@cxljs](https://github.com/cxljs) + +## 🧰 Maintenance + +- **Pool state machine**: Removed redundant `Conn.closed` atomic field in favor of the state machine's `StateClosed` ([#3783](https://github.com/redis/go-redis/pull/3783)) by [@cxljs](https://github.com/cxljs) +- **OTel SDK**: Updated OpenTelemetry SDK dependencies in `redisotel`/`redisotel-native` ([#3770](https://github.com/redis/go-redis/pull/3770)) by [@ndyakov](https://github.com/ndyakov) +- **Go 1.21+ built-ins**: Use `maps.Keys`, `slices.Collect`, `slices.Contains`, `clear()`, and `slices.SortFunc` instead of custom helpers ([#3758](https://github.com/redis/go-redis/pull/3758), [#3746](https://github.com/redis/go-redis/pull/3746)) by [@cxljs](https://github.com/cxljs) +- **HGetAll docs**: Added Go doc comment to `HGetAll` describing behavior and complexity ([#3776](https://github.com/redis/go-redis/pull/3776)) by [@0x48core](https://github.com/0x48core) +- **Docs links**: Fixed irrelevant docs links ([#3724](https://github.com/redis/go-redis/pull/3724)) by [@olzhas-sabiyev](https://github.com/olzhas-sabiyev) +- **Examples cleanup**: Removed throughput binary from examples ([#3733](https://github.com/redis/go-redis/pull/3733)) by [@ndyakov](https://github.com/ndyakov) + +## πŸ‘₯ Contributors + +We'd like to thank all the contributors who worked on this release! + +[@0x48core](https://github.com/0x48core), [@Aaditya-dubey1](https://github.com/Aaditya-dubey1), [@Copilot](https://github.com/apps/copilot-swe-agent), [@Flack74](https://github.com/Flack74), [@Jesse-Bonfire](https://github.com/Jesse-Bonfire), [@LINKIWI](https://github.com/LINKIWI), [@bigsk05](https://github.com/bigsk05), [@chaitanyabodlapati](https://github.com/chaitanyabodlapati), [@cxljs](https://github.com/cxljs), [@dariaguy](https://github.com/dariaguy), [@fatal10110](https://github.com/fatal10110), [@mwhooker](https://github.com/mwhooker), [@ndyakov](https://github.com/ndyakov), [@ofekshenawa](https://github.com/ofekshenawa), [@olde-ducke](https://github.com/olde-ducke), [@olzhas-sabiyev](https://github.com/olzhas-sabiyev), [@romanpovol](https://github.com/romanpovol), [@rubensayshi](https://github.com/rubensayshi), [@vladisa88](https://github.com/vladisa88), [@wzy9607](https://github.com/wzy9607), [@zhengjilei](https://github.com/zhengjilei) + +--- + +**Full Changelog**: https://github.com/redis/go-redis/compare/v9.18.0...v9.19.0 + # 9.18.0 (2026-02-16) ## πŸš€ Highlights diff --git a/vendor/github.com/redis/go-redis/v9/RELEASING.md b/vendor/github.com/redis/go-redis/v9/RELEASING.md index 1115db4e..033ec100 100644 --- a/vendor/github.com/redis/go-redis/v9/RELEASING.md +++ b/vendor/github.com/redis/go-redis/v9/RELEASING.md @@ -1,15 +1,146 @@ # Releasing -1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub: +This document is the runbook for cutting a go-redis release. It is intended +for maintainers with write/tag access to the repository. + +For the format and style of the release notes themselves, see +[.github/RELEASE_NOTES_TEMPLATE.md](./.github/RELEASE_NOTES_TEMPLATE.md). + +## Versioning + +go-redis follows [Semantic Versioning](https://semver.org/): + +- **Patch** (`vX.Y.Z+1`) β€” bug fixes, no API changes. +- **Minor** (`vX.Y+1.0`) β€” backwards-compatible new features, deprecations. +- **Major** (`vX+1.0.0`) β€” breaking changes. Coordinate with the team first. + +Pre-releases use `vX.Y.Z-beta.N` / `vX.Y.Z-rc.N`. + +## Pre-release checklist + +- [ ] Target branch is `master` and CI is green on the latest commit. +- [ ] All PRs intended for this release are merged. +- [ ] There are no open issues in the release milestone (if used). +- [ ] `CHANGELOG` / release notes have been considered; dependabot-only + and doc-only changes are excluded per the template. +- [ ] Confirm the next version number and decide if it's a patch / minor / major. + +## 1. Draft the release notes + +1. Open the draft release auto-generated by + [release-drafter](.github/release-drafter-config.yml) on GitHub. +2. Prepend a new section to [`RELEASE-NOTES.md`](./RELEASE-NOTES.md) using + [`.github/RELEASE_NOTES_TEMPLATE.md`](./.github/RELEASE_NOTES_TEMPLATE.md) + as the format. Keep the file in chronological order (newest first). +3. Pick 3–5 **Highlights** β€” the most user-facing, impactful changes. +4. Remove dependabot bumps and doc-only typo fixes from the lists. +5. Verify every PR has a contributor attribution and link. +6. Open a PR with just the release-notes change if you want review before + bumping versions, otherwise include it in the release PR below. + +## 2. Bump versions and open the release PR + +Create a release branch from `master`: + +```shell +git checkout master && git pull --ff-only +git checkout -b release/vX.Y.Z +``` + +Run the release script on that branch: ```shell -TAG=v1.0.0 ./scripts/release.sh +TAG=vX.Y.Z ./scripts/release.sh ``` -2. Open a pull request and wait for the build to finish. +What the script does (and explicitly does **not** do): -3. Merge the pull request and run `tag.sh` to create tags for packages: +- βœ… Validates `TAG` matches the semver regex and isn't already a git tag. +- βœ… Rewrites every `redis/go-redis*` line in every sub-module `go.mod` to + point at the new `TAG`. Trailing `// indirect` markers are preserved. +- βœ… Runs `go mod tidy -compat=1.24` in each sub-module. +- βœ… Updates the return value in [`version.go`](./version.go). +- ❌ Does **not** switch branches (runs in your current branch). +- ❌ Does **not** require a clean working tree (so you can mix it with + release-notes edits in the same branch). +- ❌ Does **not** commit, tag, or push anything. + +Review and commit the changes yourself: ```shell -TAG=v1.0.0 ./scripts/tag.sh +git diff # sanity-check the bumps +git add -u +git commit -m "chore: release vX.Y.Z" +git push origin release/vX.Y.Z ``` + +Then on GitHub: + +- [ ] Open a PR from `release/vX.Y.Z` into `master`. +- [ ] Wait for all required CI checks (build, golangci-lint, spellcheck, + doctests, e2e where applicable) to pass. +- [ ] Get at least one maintainer approval. +- [ ] Merge the PR (use a merge commit β€” the tag will point at the merge SHA). + +## 3. Tag the release + +After the release PR is merged, pull the latest `master` and dry-run the +tagger: + +```shell +git checkout master && git pull --ff-only +TAG=vX.Y.Z ./scripts/tag.sh vX.Y.Z +``` + +The script defaults to **dry-run** and prints the commands it would run. +Verify the output, then apply for real with `-t`: + +```shell +./scripts/tag.sh vX.Y.Z -t +``` + +This creates and pushes: +- The top-level tag `vX.Y.Z`. +- A per-module tag `/vX.Y.Z` for each public sub-module + (skipping `example/*` and `internal/*`). + +## 4. Publish the GitHub release + +1. On GitHub, open the draft release created by release-drafter. +2. Set the tag to `vX.Y.Z` and the target to `master`. +3. Replace the auto-generated body with the curated notes from + `RELEASE-NOTES.md` for this version. +4. For pre-releases, check **"Set as a pre-release"**. +5. Publish. + +## 5. Post-release + +- [ ] Verify the release appears on + [pkg.go.dev](https://pkg.go.dev/github.com/redis/go-redis/v9) within + a few minutes (trigger a fetch by visiting the version URL if needed). +- [ ] Announce on Discord (see the link in `CONTRIBUTING.md`). +- [ ] Close the release milestone if one was used. +- [ ] Open follow-up issues for anything deferred from this release. + +## Hotfix / patch release + +For an urgent fix on top of the latest release: + +1. Branch from the latest release tag: `git checkout -b hotfix/vX.Y.Z+1 vX.Y.Z`. +2. Cherry-pick (or re-apply) only the required fix commits. +3. Follow the normal release flow above with `TAG=vX.Y.Z+1`. +4. Make sure the fix is also present on `master` (forward-port if necessary). + +## Troubleshooting + +- **`release.sh` fails with "tag already exists"** β€” the tag has already + been created. Pick the next version, or delete the local tag first if + it was created by mistake. +- **`tag.sh` reports version mismatch in a `go.mod`** β€” a sub-module was + not updated by `release.sh`. Fix the `go.mod` manually (or re-run + `release.sh`), amend the release PR, and re-run the tagger. +- **`version.go` does not contain the tag** β€” `release.sh` did not run or + the bump was reverted. Re-run `release.sh` on the release branch. +- **pkg.go.dev does not show the new version** β€” visit + `https://pkg.go.dev/github.com/redis/go-redis/v9@vX.Y.Z` once to trigger + a fetch from the module proxy. diff --git a/vendor/github.com/redis/go-redis/v9/array_commands.go b/vendor/github.com/redis/go-redis/v9/array_commands.go new file mode 100644 index 00000000..71037dfd --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/array_commands.go @@ -0,0 +1,387 @@ +package redis + +import ( + "context" +) + +// note: the APIs is experimental and may be subject to change. +// +// ArrayCmdable defines the interface for Redis Array data structure commands +// available in Redis 8.8.0+. +// +// Redis array supports index range [0, math.MaxUint64-1), so index parameters use uint64. +type ArrayCmdable interface { + ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd + ARGet(ctx context.Context, key string, index uint64) *StringCmd + ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd + ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd + ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd + ARInsert(ctx context.Context, key string, values ...string) *UintCmd + ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd + ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd + ARLen(ctx context.Context, key string) *UintCmd + ARCount(ctx context.Context, key string) *UintCmd + ARNext(ctx context.Context, key string) *UintCmd + ARSeek(ctx context.Context, key string, index uint64) *IntCmd + ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd + ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd + ARScan(ctx context.Context, key string, start, end uint64, args *ARScanArgs) *AREntrySliceCmd + AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd + AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd + AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd + AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd + AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd + AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd + AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd + AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd + ARGrep(ctx context.Context, key string, start, end string, args *ARGrepArgs) *UintSliceCmd + ARGrepWithValues(ctx context.Context, key string, start, end string, args *ARGrepArgs) *AREntrySliceCmd + ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd + ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd +} + +// AREntry represents an index-value pair for ARMSET. +type AREntry struct { + Index uint64 + Value string +} + +// ARRange represents a start-end range for ARDELRANGE. +type ARRange struct { + Start uint64 + End uint64 +} + +// ARScanArgs contains optional arguments for ARSCAN. +type ARScanArgs struct { + Limit uint64 +} + +// ARGrepPredicateType defines the type of predicate for ARGREP. +type ARGrepPredicateType string + +const ( + ARGrepExact ARGrepPredicateType = "EXACT" + ARGrepMatch ARGrepPredicateType = "MATCH" + ARGrepGlob ARGrepPredicateType = "GLOB" + ARGrepRegex ARGrepPredicateType = "RE" +) + +// ARGrepPredicate represents a search predicate for ARGREP. +type ARGrepPredicate struct { + Type ARGrepPredicateType + Value string +} + +// ARGrepArgs contains optional arguments for ARGREP. +// Redis ARGREP defaults to OR when multiple predicates are given. +// Set CombineAnd to true to combine predicates with AND instead. +type ARGrepArgs struct { + Predicates []ARGrepPredicate + CombineAnd bool + Limit uint64 + NoCase bool +} + +// ARSet sets one or more contiguous values starting at an index in an array. +// Returns the number of new slots that were set (previously empty). +func (c cmdable) ARSet(ctx context.Context, key string, index uint64, values ...string) *IntCmd { + args := make([]any, 3, 3+len(values)) + args[0] = "arset" + args[1] = key + args[2] = index + for _, v := range values { + args = append(args, v) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARGet gets the value at an index in an array. +// Returns redis.Nil if the key or index does not exist. +func (c cmdable) ARGet(ctx context.Context, key string, index uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arget", key, index) + _ = c(ctx, cmd) + return cmd +} + +// ARGetRange gets values in a range of indexes. +// Returns values in the range, with nil for unset indexes. +func (c cmdable) ARGetRange(ctx context.Context, key string, start, end uint64) *SliceCmd { + cmd := NewSliceCmd(ctx, "argetrange", key, start, end) + _ = c(ctx, cmd) + return cmd +} + +// ARMGet gets values at multiple indexes in an array. +// Returns values at the specified indexes, with nil for unset indexes. +func (c cmdable) ARMGet(ctx context.Context, key string, indexes ...uint64) *SliceCmd { + args := make([]any, 2+len(indexes)) + args[0] = "armget" + args[1] = key + for i, idx := range indexes { + args[2+i] = idx + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARMSet sets multiple index-value pairs in an array. +// Returns the number of new slots that were set (previously empty). +func (c cmdable) ARMSet(ctx context.Context, key string, members ...AREntry) *IntCmd { + args := make([]any, 2, 2+2*len(members)) + args[0] = "armset" + args[1] = key + for _, m := range members { + args = append(args, m.Index, m.Value) + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARInsert inserts one or more values at consecutive indexes. +// Returns the last index where a value was inserted. +func (c cmdable) ARInsert(ctx context.Context, key string, values ...string) *UintCmd { + args := make([]any, 2, 2+len(values)) + args[0] = "arinsert" + args[1] = key + for _, v := range values { + args = append(args, v) + } + cmd := NewUintCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARDel deletes elements at the specified indexes in an array. +// Returns the number of elements deleted. +func (c cmdable) ARDel(ctx context.Context, key string, indexes ...uint64) *IntCmd { + args := make([]any, 2+len(indexes)) + args[0] = "ardel" + args[1] = key + for i, idx := range indexes { + args[2+i] = idx + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARDelRange deletes elements in one or more ranges. +// Returns the number of elements deleted. +func (c cmdable) ARDelRange(ctx context.Context, key string, ranges ...ARRange) *UintCmd { + args := make([]any, 2, 2+2*len(ranges)) + args[0] = "ardelrange" + args[1] = key + for _, r := range ranges { + args = append(args, r.Start, r.End) + } + cmd := NewUintCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARLen returns the length of an array (max index + 1). +// Returns 0 if the key does not exist. +func (c cmdable) ARLen(ctx context.Context, key string) *UintCmd { + cmd := NewUintCmd(ctx, "arlen", key) + _ = c(ctx, cmd) + return cmd +} + +// ARCount returns the number of non-empty elements in an array. +// Returns 0 if the key does not exist. +func (c cmdable) ARCount(ctx context.Context, key string) *UintCmd { + cmd := NewUintCmd(ctx, "arcount", key) + _ = c(ctx, cmd) + return cmd +} + +// ARNext returns the next index ARINSERT would use. +// Returns 0 for missing keys or when no insert happened yet. +// Returns nil when the insertion cursor is exhausted / would overflow. +func (c cmdable) ARNext(ctx context.Context, key string) *UintCmd { + cmd := NewUintCmd(ctx, "arnext", key) + _ = c(ctx, cmd) + return cmd +} + +// ARSeek sets the ARINSERT / ARRING cursor to a specific index. +// Returns 1 if the cursor was set, 0 if the key does not exist. +func (c cmdable) ARSeek(ctx context.Context, key string, index uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arseek", key, index) + _ = c(ctx, cmd) + return cmd +} + +// ARInfo returns metadata about an array. +func (c cmdable) ARInfo(ctx context.Context, key string) *MapStringInterfaceCmd { + cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key) + _ = c(ctx, cmd) + return cmd +} + +// ARInfoFull returns detailed metadata about an array including slice statistics. +func (c cmdable) ARInfoFull(ctx context.Context, key string) *MapStringInterfaceCmd { + cmd := NewMapStringInterfaceCmd(ctx, "arinfo", key, "full") + _ = c(ctx, cmd) + return cmd +} + +// ARScan iterates existing elements in a range, returning index-value pairs. +func (c cmdable) ARScan(ctx context.Context, key string, start, end uint64, scanArgs *ARScanArgs) *AREntrySliceCmd { + args := make([]any, 4, 6) + args[0], args[1], args[2], args[3] = "arscan", key, start, end + if scanArgs != nil && scanArgs.Limit > 0 { + args = append(args, "limit", scanArgs.Limit) + } + cmd := NewAREntrySliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// AROpSum returns the sum of numeric elements in a range. +func (c cmdable) AROpSum(ctx context.Context, key string, start, end uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arop", key, start, end, "SUM") + _ = c(ctx, cmd) + return cmd +} + +// AROpMin returns the minimum numeric element in a range. +func (c cmdable) AROpMin(ctx context.Context, key string, start, end uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arop", key, start, end, "MIN") + _ = c(ctx, cmd) + return cmd +} + +// AROpMax returns the maximum numeric element in a range. +func (c cmdable) AROpMax(ctx context.Context, key string, start, end uint64) *StringCmd { + cmd := NewStringCmd(ctx, "arop", key, start, end, "MAX") + _ = c(ctx, cmd) + return cmd +} + +// AROpAnd returns the bitwise AND of integer elements in a range. +func (c cmdable) AROpAnd(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "AND") + _ = c(ctx, cmd) + return cmd +} + +// AROpOr returns the bitwise OR of integer elements in a range. +func (c cmdable) AROpOr(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "OR") + _ = c(ctx, cmd) + return cmd +} + +// AROpXor returns the bitwise XOR of integer elements in a range. +func (c cmdable) AROpXor(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "XOR") + _ = c(ctx, cmd) + return cmd +} + +// AROpMatch returns the count of elements matching a target string in a range. +func (c cmdable) AROpMatch(ctx context.Context, key string, start, end uint64, value string) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "MATCH", value) + _ = c(ctx, cmd) + return cmd +} + +// AROpUsed returns the count of non-empty slots in a range. +func (c cmdable) AROpUsed(ctx context.Context, key string, start, end uint64) *IntCmd { + cmd := NewIntCmd(ctx, "arop", key, start, end, "USED") + _ = c(ctx, cmd) + return cmd +} + +// ARGrep searches array elements in a range using textual predicates. +// Returns matching indexes only. Use ARGrepWithValues to also get the values. +func (c cmdable) ARGrep(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *UintSliceCmd { + args := make([]any, 4, 4+grepArgs.Len()) + args[0], args[1], args[2], args[3] = "argrep", key, start, end + args = grepArgs.Append(args) + cmd := NewUintSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARGrepWithValues searches array elements in a range using textual predicates. +// Returns matching indexes and their values as index-value pairs. +func (c cmdable) ARGrepWithValues(ctx context.Context, key string, start, end string, grepArgs *ARGrepArgs) *AREntrySliceCmd { + args := make([]any, 4, 5+grepArgs.Len()) + args[0], args[1], args[2], args[3] = "argrep", key, start, end + args = grepArgs.Append(args) + args = append(args, "withvalues") + cmd := NewAREntrySliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func (args *ARGrepArgs) Len() int { + if args == nil { + return 0 + } + n := 2 * len(args.Predicates) + if args.CombineAnd { + n++ + } + if args.Limit > 0 { + n += 2 + } + if args.NoCase { + n++ + } + return n +} + +func (args *ARGrepArgs) Append(a []any) []any { + if args == nil { + return a + } + for _, p := range args.Predicates { + a = append(a, string(p.Type), p.Value) + } + if args.CombineAnd { + a = append(a, "and") + } + if args.Limit > 0 { + a = append(a, "limit", args.Limit) + } + if args.NoCase { + a = append(a, "nocase") + } + return a +} + +// ARRing inserts values into a ring buffer of specified size, wrapping and truncating as needed. +// Returns the last index where a value was inserted. +func (c cmdable) ARRing(ctx context.Context, key string, size uint64, values ...string) *UintCmd { + args := make([]any, 3, 3+len(values)) + args[0] = "arring" + args[1] = key + args[2] = size + for _, v := range values { + args = append(args, v) + } + cmd := NewUintCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// ARLastItems returns the most recently inserted elements. +// When rev is true, returns items in reverse order. +func (c cmdable) ARLastItems(ctx context.Context, key string, count uint64, rev bool) *SliceCmd { + args := make([]any, 3, 4) + args[0], args[1], args[2] = "arlastitems", key, count + if rev { + args = append(args, "rev") + } + cmd := NewSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} diff --git a/vendor/github.com/redis/go-redis/v9/auth/auth.go b/vendor/github.com/redis/go-redis/v9/auth/auth.go index 1f5c8022..21667a12 100644 --- a/vendor/github.com/redis/go-redis/v9/auth/auth.go +++ b/vendor/github.com/redis/go-redis/v9/auth/auth.go @@ -9,12 +9,30 @@ type StreamingCredentialsProvider interface { // Subscribe subscribes to the credentials provider for updates. // It returns the current credentials, a cancel function to unsubscribe from the provider, // and an error if any. + // + // Implementations MUST be idempotent with respect to listener identity: + // subscribing the same listener value more than once must not produce + // duplicate notifications and must not create multiple independent + // subscriptions that each need to be cancelled separately. Every + // UnsubscribeFunc returned for a given listener must cancel that + // listener's subscription; calling any one of them must be sufficient to + // stop updates to that listener, and calling subsequent ones must be a + // safe no-op. Callers (including go-redis internals) may retain only + // the most recently returned UnsubscribeFunc and rely on it to fully + // unsubscribe the listener. + // // TODO(ndyakov): Should we add context to the Subscribe method? Subscribe(listener CredentialsListener) (Credentials, UnsubscribeFunc, error) } // UnsubscribeFunc is a function that is used to cancel the subscription to the credentials provider. // It is used to unsubscribe from the provider when the credentials are no longer needed. +// +// Per the StreamingCredentialsProvider.Subscribe contract, if the same +// listener is subscribed multiple times, every UnsubscribeFunc returned for +// that listener must fully unsubscribe it on first invocation, and +// subsequent invocations (from any of the equivalent UnsubscribeFuncs) must +// be a safe no-op. type UnsubscribeFunc func() error // CredentialsListener is an interface that defines the methods for a credentials listener. diff --git a/vendor/github.com/redis/go-redis/v9/command.go b/vendor/github.com/redis/go-redis/v9/command.go index a2a2f051..57a26c8b 100644 --- a/vendor/github.com/redis/go-redis/v9/command.go +++ b/vendor/github.com/redis/go-redis/v9/command.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "fmt" + "io" "maps" "net" "regexp" @@ -65,6 +66,7 @@ var keylessCommands = map[string]struct{}{ "subscribe": {}, "swapdb": {}, "sync": {}, + "time": {}, "unsubscribe": {}, "unwatch": {}, "wait": {}, @@ -107,6 +109,7 @@ const ( CmdTypeXPending CmdTypeXPendingExt CmdTypeXAutoClaim + CmdTypeXAutoClaimWithDeleted CmdTypeXAutoClaimJustID CmdTypeXInfoConsumers CmdTypeXInfoGroups @@ -153,6 +156,11 @@ const ( CmdTypeTSTimestampValue CmdTypeTSTimestampValueSlice CmdTypeHotKeys + CmdTypeIncrEXInt + CmdTypeIncrEXFloat + CmdTypeUint + CmdTypeUintSlice + CmdTypeAREntrySlice ) type ( @@ -161,6 +169,12 @@ type ( start string } + CmdTypeXAutoClaimWithDeletedValue struct { + messages []XMessage + start string + deletedIDs []string + } + CmdTypeXAutoClaimJustIDValue struct { ids []string start string @@ -214,6 +228,11 @@ type Cmder interface { SetErr(error) Err() error + // NoRetry returns true if the command should not be retried on failure. + // Commands that write directly to an io.Writer should return true since + // partial writes cannot be undone on retry. + NoRetry() bool + // GetCmdType returns the command type for fast value extraction GetCmdType() CmdType } @@ -235,6 +254,18 @@ func cmdsFirstErr(cmds []Cmder) error { return nil } +// cmdsContainNoRetry returns true if any command in the slice has NoRetry() == true. +// If a pipeline contains a non-retryable command (e.g., RawWriteToCmd), the entire +// pipeline must not be retried to prevent data corruption from partial writes. +func cmdsContainNoRetry(cmds []Cmder) bool { + for _, cmd := range cmds { + if cmd.NoRetry() { + return true + } + } + return false +} + func writeCmds(wr *proto.Writer, cmds []Cmder) error { for _, cmd := range cmds { if err := writeCmd(wr, cmd); err != nil { @@ -248,10 +279,10 @@ func writeCmd(wr *proto.Writer, cmd Cmder) error { return wr.WriteArgs(cmd.Args()) } -// cmdFirstKeyPos returns the position of the first key in the command's arguments. -// If the command does not have a key, it returns 0. -// TODO: Use the data in CommandInfo to determine the first key position. -func cmdFirstKeyPos(cmd Cmder) int { +// cmdFirstKeyPosWithInfo returns the first key position in a command's args (0 if none). +// Uses CommandInfo.FirstKeyPos when available (via cache peek, no network call), falling +// back to a hardcoded table. eval/evalsha variants are resolved from the runtime numkeys arg. +func cmdFirstKeyPosWithInfo(cmd Cmder, info *CommandInfo) int { if pos := cmd.firstKeyPos(); pos != 0 { return int(pos) } @@ -270,14 +301,20 @@ func cmdFirstKeyPos(cmd Cmder) int { } return 0 - case "publish": - return 1 case "memory": // https://github.com/redis/redis/issues/7493 if cmd.stringArg(1) == "usage" { return 2 } + // CommandInfo (if available) gives the correct answer + // otherwise the hardcoded fallback applies. } + + // Use CommandInfo cache when warm (in-memory only, no extra round-trips). + if info != nil { + return int(info.FirstKeyPos) + } + return 1 } @@ -397,6 +434,14 @@ func (cmd *baseCmd) readRawReply(rd *proto.Reader) (err error) { return err } +// NoRetry returns true if the command should not be retried on failure. +// By default, commands can be retried. Commands that write directly to an +// io.Writer (like RawWriteToCmd) should override this to return true since +// partial writes cannot be undone on retry. +func (cmd *baseCmd) NoRetry() bool { + return false +} + func (cmd *baseCmd) GetCmdType() CmdType { return cmd.cmdType } @@ -719,6 +764,122 @@ func (cmd *Cmd) Clone() Cmder { //------------------------------------------------------------------------------ +// RawCmd returns raw RESP protocol bytes without parsing. +type RawCmd struct { + baseCmd + val []byte +} + +var _ Cmder = (*RawCmd)(nil) + +func NewRawCmd(ctx context.Context, args ...interface{}) *RawCmd { + return &RawCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeGeneric, + }, + } +} + +func (cmd *RawCmd) SetVal(val []byte) { + cmd.val = val +} + +func (cmd *RawCmd) Val() []byte { + return cmd.val +} + +func (cmd *RawCmd) Result() ([]byte, error) { + return cmd.val, cmd.err +} + +func (cmd *RawCmd) Bytes() ([]byte, error) { + return cmd.val, cmd.err +} + +func (cmd *RawCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *RawCmd) readReply(rd *proto.Reader) (err error) { + cmd.val, err = rd.ReadRawReply() + return err +} + +func (cmd *RawCmd) Clone() Cmder { + var val []byte + if cmd.val != nil { + val = make([]byte, len(cmd.val)) + copy(val, cmd.val) + } + return &RawCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +//------------------------------------------------------------------------------ + +// RawWriteToCmd streams raw RESP protocol bytes directly to an io.Writer without intermediate allocations. +type RawWriteToCmd struct { + baseCmd + w io.Writer + written int64 +} + +var _ Cmder = (*RawWriteToCmd)(nil) + +func NewRawWriteToCmd(ctx context.Context, w io.Writer, args ...interface{}) *RawWriteToCmd { + return &RawWriteToCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeGeneric, + }, + w: w, + } +} + +func (cmd *RawWriteToCmd) SetVal(written int64) { + cmd.written = written +} + +func (cmd *RawWriteToCmd) Val() int64 { + return cmd.written +} + +func (cmd *RawWriteToCmd) Result() (int64, error) { + return cmd.written, cmd.err +} + +func (cmd *RawWriteToCmd) String() string { + return cmdString(cmd, cmd.written) +} + +func (cmd *RawWriteToCmd) readReply(rd *proto.Reader) (err error) { + cmd.written, err = rd.ReadRawReplyWriteTo(cmd.w) + return err +} + +// NoRetry returns true because RawWriteToCmd writes directly to an io.Writer. +// If a retry occurs, partial data from failed attempts would be appended to +// the writer, causing data corruption. The caller must handle retries manually +// if needed, using a fresh writer for each attempt. +func (cmd *RawWriteToCmd) NoRetry() bool { + return true +} + +func (cmd *RawWriteToCmd) Clone() Cmder { + return &RawWriteToCmd{ + baseCmd: cmd.cloneBaseCmd(), + w: cmd.w, + written: cmd.written, + } +} + +//------------------------------------------------------------------------------ + type SliceCmd struct { baseCmd @@ -894,6 +1055,52 @@ func (cmd *IntCmd) Clone() Cmder { } } +type UintCmd struct { + baseCmd + + val uint64 +} + +var _ Cmder = (*UintCmd)(nil) + +func NewUintCmd(ctx context.Context, args ...any) *UintCmd { + return &UintCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeUint, + }, + } +} + +func (cmd *UintCmd) SetVal(val uint64) { + cmd.val = val +} + +func (cmd *UintCmd) Val() uint64 { + return cmd.val +} + +func (cmd *UintCmd) Result() (uint64, error) { + return cmd.val, cmd.err +} + +func (cmd *UintCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *UintCmd) readReply(rd *proto.Reader) (err error) { + cmd.val, err = rd.ReadUint() + return err +} + +func (cmd *UintCmd) Clone() Cmder { + return &UintCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + //------------------------------------------------------------------------------ // DigestCmd is a command that returns a uint64 xxh3 hash digest. @@ -1025,6 +1232,66 @@ func (cmd *IntSliceCmd) Clone() Cmder { } } +type UintSliceCmd struct { + baseCmd + + val []uint64 +} + +var _ Cmder = (*UintSliceCmd)(nil) + +func NewUintSliceCmd(ctx context.Context, args ...any) *UintSliceCmd { + return &UintSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeUintSlice, + }, + } +} + +func (cmd *UintSliceCmd) SetVal(val []uint64) { + cmd.val = val +} + +func (cmd *UintSliceCmd) Val() []uint64 { + return cmd.val +} + +func (cmd *UintSliceCmd) Result() ([]uint64, error) { + return cmd.val, cmd.err +} + +func (cmd *UintSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *UintSliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val = make([]uint64, n) + for i := range cmd.val { + if cmd.val[i], err = rd.ReadUint(); err != nil { + return err + } + } + return nil +} + +func (cmd *UintSliceCmd) Clone() Cmder { + var val []uint64 + if cmd.val != nil { + val = make([]uint64, len(cmd.val)) + copy(val, cmd.val) + } + return &UintSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + //------------------------------------------------------------------------------ type DurationCmd struct { @@ -1502,6 +1769,87 @@ func (cmd *StringSliceCmd) Clone() Cmder { //------------------------------------------------------------------------------ +// StringSliceSliceCmd returns a slice of string slices ([][]string). +// This is used for commands like VLINKS that return an array of arrays. +type StringSliceSliceCmd struct { + baseCmd + + val [][]string +} + +var _ Cmder = (*StringSliceSliceCmd)(nil) + +func NewStringSliceSliceCmd(ctx context.Context, args ...any) *StringSliceSliceCmd { + return &StringSliceSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *StringSliceSliceCmd) SetVal(val [][]string) { + cmd.val = val +} + +func (cmd *StringSliceSliceCmd) Val() [][]string { + return cmd.val +} + +func (cmd *StringSliceSliceCmd) Result() ([][]string, error) { + return cmd.val, cmd.err +} + +func (cmd *StringSliceSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *StringSliceSliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val = make([][]string, n) + for i := range n { + // Read inner array + innerN, err := rd.ReadArrayLen() + if err != nil { + return err + } + cmd.val[i] = make([]string, innerN) + for j := range innerN { + switch s, err := rd.ReadString(); { + case err == Nil: + cmd.val[i][j] = "" + case err != nil: + return err + default: + cmd.val[i][j] = s + } + } + } + return nil +} + +func (cmd *StringSliceSliceCmd) Clone() Cmder { + var val [][]string + if cmd.val != nil { + val = make([][]string, len(cmd.val)) + for i, slice := range cmd.val { + if slice != nil { + val[i] = make([]string, len(slice)) + copy(val[i], slice) + } + } + } + return &StringSliceSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +//------------------------------------------------------------------------------ + type KeyValue struct { Key string Value string @@ -2075,10 +2423,7 @@ func (cmd *XMessageSliceCmd) Clone() Cmder { ID: msg.ID, } if msg.Values != nil { - val[i].Values = make(map[string]interface{}, len(msg.Values)) - for k, v := range msg.Values { - val[i].Values[k] = v - } + val[i].Values = maps.Clone(msg.Values) } } } @@ -2528,9 +2873,7 @@ func (cmd *XAutoClaimCmd) readReply(rd *proto.Reader) error { } if n >= 3 { - if err := rd.DiscardNext(); err != nil { - return err - } + return rd.DiscardNext() } return nil @@ -2561,43 +2904,45 @@ func (cmd *XAutoClaimCmd) Clone() Cmder { //------------------------------------------------------------------------------ -type XAutoClaimJustIDCmd struct { +type XAutoClaimWithDeletedCmd struct { baseCmd - start string - val []string + start string + val []XMessage + deletedIDs []string } -var _ Cmder = (*XAutoClaimJustIDCmd)(nil) +var _ Cmder = (*XAutoClaimWithDeletedCmd)(nil) -func NewXAutoClaimJustIDCmd(ctx context.Context, args ...interface{}) *XAutoClaimJustIDCmd { - return &XAutoClaimJustIDCmd{ +func NewXAutoClaimWithDeletedCmd(ctx context.Context, args ...interface{}) *XAutoClaimWithDeletedCmd { + return &XAutoClaimWithDeletedCmd{ baseCmd: baseCmd{ ctx: ctx, args: args, - cmdType: CmdTypeXAutoClaimJustID, + cmdType: CmdTypeXAutoClaimWithDeleted, }, } } -func (cmd *XAutoClaimJustIDCmd) SetVal(val []string, start string) { +func (cmd *XAutoClaimWithDeletedCmd) SetVal(val []XMessage, start string, deletedIDs []string) { cmd.val = val cmd.start = start + cmd.deletedIDs = deletedIDs } -func (cmd *XAutoClaimJustIDCmd) Val() (ids []string, start string) { - return cmd.val, cmd.start +func (cmd *XAutoClaimWithDeletedCmd) Val() (messages []XMessage, start string, deletedIDs []string) { + return cmd.val, cmd.start, cmd.deletedIDs } -func (cmd *XAutoClaimJustIDCmd) Result() (ids []string, start string, err error) { - return cmd.val, cmd.start, cmd.err +func (cmd *XAutoClaimWithDeletedCmd) Result() (messages []XMessage, start string, deletedIDs []string, err error) { + return cmd.val, cmd.start, cmd.deletedIDs, cmd.err } -func (cmd *XAutoClaimJustIDCmd) String() string { +func (cmd *XAutoClaimWithDeletedCmd) String() string { return cmdString(cmd, cmd.val) } -func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { +func (cmd *XAutoClaimWithDeletedCmd) readReply(rd *proto.Reader) error { n, err := rd.ReadArrayLen() if err != nil { return err @@ -2608,7 +2953,7 @@ func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { 3: // Redis 7: // ok default: - return fmt.Errorf("redis: got %d elements in XAutoClaimJustID reply, wanted 2/3", n) + return fmt.Errorf("redis: got %d elements in XAutoClaim reply, wanted 2/3", n) } cmd.start, err = rd.ReadString() @@ -2616,33 +2961,144 @@ func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { return err } + cmd.val, err = readXMessageSlice(rd) + if err != nil { + return err + } + + if n < 3 { + return nil + } + nn, err := rd.ReadArrayLen() if err != nil { return err } - cmd.val = make([]string, nn) + cmd.deletedIDs = make([]string, nn) for i := 0; i < nn; i++ { - cmd.val[i], err = rd.ReadString() + cmd.deletedIDs[i], err = rd.ReadString() if err != nil { return err } } - if n >= 3 { - if err := rd.DiscardNext(); err != nil { - return err - } - } - return nil } -func (cmd *XAutoClaimJustIDCmd) Clone() Cmder { - var val []string +func (cmd *XAutoClaimWithDeletedCmd) Clone() Cmder { + var val []XMessage if cmd.val != nil { - val = make([]string, len(cmd.val)) - copy(val, cmd.val) + val = make([]XMessage, len(cmd.val)) + for i, msg := range cmd.val { + val[i] = XMessage{ + ID: msg.ID, + } + if msg.Values != nil { + val[i].Values = make(map[string]interface{}, len(msg.Values)) + for k, v := range msg.Values { + val[i].Values[k] = v + } + } + } + } + var deletedIDs []string + if cmd.deletedIDs != nil { + deletedIDs = make([]string, len(cmd.deletedIDs)) + copy(deletedIDs, cmd.deletedIDs) + } + return &XAutoClaimWithDeletedCmd{ + baseCmd: cmd.cloneBaseCmd(), + start: cmd.start, + val: val, + deletedIDs: deletedIDs, + } +} + +//------------------------------------------------------------------------------ + +type XAutoClaimJustIDCmd struct { + baseCmd + + start string + val []string +} + +var _ Cmder = (*XAutoClaimJustIDCmd)(nil) + +func NewXAutoClaimJustIDCmd(ctx context.Context, args ...interface{}) *XAutoClaimJustIDCmd { + return &XAutoClaimJustIDCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeXAutoClaimJustID, + }, + } +} + +func (cmd *XAutoClaimJustIDCmd) SetVal(val []string, start string) { + cmd.val = val + cmd.start = start +} + +func (cmd *XAutoClaimJustIDCmd) Val() (ids []string, start string) { + return cmd.val, cmd.start +} + +func (cmd *XAutoClaimJustIDCmd) Result() (ids []string, start string, err error) { + return cmd.val, cmd.start, cmd.err +} + +func (cmd *XAutoClaimJustIDCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *XAutoClaimJustIDCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + + switch n { + case 2, // Redis 6 + 3: // Redis 7: + // ok + default: + return fmt.Errorf("redis: got %d elements in XAutoClaimJustID reply, wanted 2/3", n) + } + + cmd.start, err = rd.ReadString() + if err != nil { + return err + } + + nn, err := rd.ReadArrayLen() + if err != nil { + return err + } + + cmd.val = make([]string, nn) + for i := 0; i < nn; i++ { + cmd.val[i], err = rd.ReadString() + if err != nil { + return err + } + } + + if n >= 3 { + if err := rd.DiscardNext(); err != nil { + return err + } + } + + return nil +} + +func (cmd *XAutoClaimJustIDCmd) Clone() Cmder { + var val []string + if cmd.val != nil { + val = make([]string, len(cmd.val)) + copy(val, cmd.val) } return &XAutoClaimJustIDCmd{ baseCmd: cmd.cloneBaseCmd(), @@ -2727,7 +3183,10 @@ func (cmd *XInfoConsumersCmd) readReply(rd *proto.Reader) error { inactive, err = rd.ReadInt() cmd.val[i].Inactive = time.Duration(inactive) * time.Millisecond default: - return fmt.Errorf("redis: unexpected content %s in XINFO CONSUMERS reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } if err != nil { return err @@ -2856,7 +3315,10 @@ func (cmd *XInfoGroupsCmd) readReply(rd *proto.Reader) error { group.Lag = -1 } default: - return fmt.Errorf("redis: unexpected key %q in XINFO GROUPS reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } } } @@ -3025,7 +3487,10 @@ func (cmd *XInfoStreamCmd) readReply(rd *proto.Reader) error { return err } default: - return fmt.Errorf("redis: unexpected key %q in XINFO STREAM reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } } return nil @@ -3101,6 +3566,7 @@ type XInfoStreamGroup struct { EntriesRead int64 Lag int64 PelCount int64 + NackedCount uint64 // redis version 8.8, number of NACK'd messages in the group Pending []XInfoStreamGroupPending Consumers []XInfoStreamConsumer } @@ -3245,7 +3711,10 @@ func (cmd *XInfoStreamFullCmd) readReply(rd *proto.Reader) error { return err } default: - return fmt.Errorf("redis: unexpected key %q in XINFO STREAM FULL reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return err + } } } return nil @@ -3299,6 +3768,11 @@ func readStreamGroups(rd *proto.Reader) ([]XInfoStreamGroup, error) { if err != nil { return nil, err } + case "nacked-count": + group.NackedCount, err = rd.ReadUint() + if err != nil { + return nil, err + } case "pending": group.Pending, err = readXInfoStreamGroupPending(rd) if err != nil { @@ -3310,7 +3784,10 @@ func readStreamGroups(rd *proto.Reader) ([]XInfoStreamGroup, error) { return nil, err } default: - return nil, fmt.Errorf("redis: unexpected key %q in XINFO STREAM FULL reply", key) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return nil, err + } } } @@ -3435,8 +3912,10 @@ func readXInfoStreamConsumers(rd *proto.Reader) ([]XInfoStreamConsumer, error) { c.Pending = append(c.Pending, p) } default: - return nil, fmt.Errorf("redis: unexpected content %s "+ - "in XINFO STREAM FULL reply", cKey) + // skip unknown fields + if err = rd.DiscardNext(); err != nil { + return nil, err + } } if err != nil { return nil, err @@ -4615,7 +5094,7 @@ type cmdsInfoCache struct { fn func(ctx context.Context) (map[string]*CommandInfo, error) once internal.Once - refreshLock sync.Mutex + refreshLock sync.RWMutex cmds map[string]*CommandInfo } @@ -4655,9 +5134,25 @@ func (c *cmdsInfoCache) Refresh() { c.once = internal.Once{} } +// Peek returns the cached CommandInfo map without triggering a Redis round-trip. +// Returns nil when the cache is cold; callers should fall back to other heuristics. +// Note: during the very first Get() (initial population) this call will block on +// the writer lock. After that, concurrent Peek() calls do not block each other. +// The returned map and its entries MUST NOT be mutated by the caller. +func (c *cmdsInfoCache) Peek() map[string]*CommandInfo { + if c == nil { + return nil + } + c.refreshLock.RLock() + defer c.refreshLock.RUnlock() + return c.cmds +} + // ------------------------------------------------------------------------------ -const requestPolicy = "request_policy" -const responsePolicy = "response_policy" +const ( + requestPolicy = "request_policy" + responsePolicy = "response_policy" +) func parseCommandPolicies(commandInfoTips map[string]string, firstKeyPos int8) *routing.CommandPolicy { req := routing.ReqDefault @@ -6630,7 +7125,9 @@ func (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error { case "health": cmd.val[i].Nodes[k].Health, err = rd.ReadString() default: - return fmt.Errorf("redis: unexpected key %q in CLUSTER SHARDS node reply", nodeKey) + if err = rd.DiscardNext(); err != nil { + return err + } } if err != nil { @@ -6639,7 +7136,9 @@ func (cmd *ClusterShardsCmd) readReply(rd *proto.Reader) error { } } default: - return fmt.Errorf("redis: unexpected key %q in CLUSTER SHARDS reply", key) + if err = rd.DiscardNext(); err != nil { + return err + } } } } @@ -6833,6 +7332,9 @@ type ClientInfo struct { Resp int // redis version 7.0, client RESP protocol version LibName string // redis version 7.2, client library name LibVer string // redis version 7.2, client library version + ReadEvents uint64 // redis version 8.8, number of read events processed + AvgPipelineLenSum uint64 // redis version 8.8, sum of pipeline lengths + AvgPipelineLenCnt uint64 // redis version 8.8, count of pipeline operations } type ClientInfoCmd struct { @@ -7013,8 +7515,14 @@ func parseClientInfo(txt string) (info *ClientInfo, err error) { info.LibVer = val case "io-thread": info.IoThread, err = strconv.Atoi(val) + case "read-events": + info.ReadEvents, err = strconv.ParseUint(val, 10, 64) + case "avg-pipeline-len-sum": + info.AvgPipelineLenSum, err = strconv.ParseUint(val, 10, 64) + case "avg-pipeline-len-cnt": + info.AvgPipelineLenCnt, err = strconv.ParseUint(val, 10, 64) default: - return nil, fmt.Errorf("redis: unexpected client info key(%s)", key) + // skip unknown fields } if err != nil { @@ -7061,6 +7569,9 @@ func (cmd *ClientInfoCmd) Clone() Cmder { Resp: cmd.val.Resp, LibName: cmd.val.LibName, LibVer: cmd.val.LibVer, + ReadEvents: cmd.val.ReadEvents, + AvgPipelineLenSum: cmd.val.AvgPipelineLenSum, + AvgPipelineLenCnt: cmd.val.AvgPipelineLenCnt, } } return &ClientInfoCmd{ @@ -7167,7 +7678,10 @@ func (cmd *ACLLogCmd) readReply(rd *proto.Reader) error { case "timestamp-last-updated": entry.TimestampLastUpdated, err = rd.ReadInt() default: - return fmt.Errorf("redis: unexpected key %q in ACL LOG reply", key) + // skip unknown fields + if err := rd.DiscardNext(); err != nil { + return err + } } if err != nil { @@ -7231,6 +7745,9 @@ func (cmd *ACLLogCmd) Clone() Cmder { Resp: entry.ClientInfo.Resp, LibName: entry.ClientInfo.LibName, LibVer: entry.ClientInfo.LibVer, + ReadEvents: entry.ClientInfo.ReadEvents, + AvgPipelineLenSum: entry.ClientInfo.AvgPipelineLenSum, + AvgPipelineLenCnt: entry.ClientInfo.AvgPipelineLenCnt, } } } @@ -7451,7 +7968,7 @@ type VectorScoreSliceCmd struct { var _ Cmder = (*VectorScoreSliceCmd)(nil) -func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { +func NewVectorScoreSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { return &VectorScoreSliceCmd{ baseCmd: baseCmd{ ctx: ctx, @@ -7460,6 +7977,11 @@ func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCm } } +// NewVectorInfoSliceCmd is an alias for NewVectorScoreSliceCmd kept for backwards compatibility. +func NewVectorInfoSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceCmd { + return NewVectorScoreSliceCmd(ctx, args...) +} + func (cmd *VectorScoreSliceCmd) SetVal(val []VectorScore) { cmd.val = val } @@ -7477,11 +7999,29 @@ func (cmd *VectorScoreSliceCmd) String() string { } func (cmd *VectorScoreSliceCmd) readReply(rd *proto.Reader) error { - n, err := rd.ReadMapLen() + typ, err := rd.PeekReplyType() if err != nil { return err } + var n int + if typ == proto.RespMap { + n, err = rd.ReadMapLen() + if err != nil { + return err + } + } else { + // RESP2 returns a flat array [name, score, name, score, ...] + n, err = rd.ReadArrayLen() + if err != nil { + return err + } + if n%2 != 0 { + return fmt.Errorf("redis: VectorScoreSliceCmd expects even number of elements, got %d", n) + } + n /= 2 + } + cmd.val = make([]VectorScore, n) for i := 0; i < n; i++ { name, err := rd.ReadString() @@ -7507,6 +8047,322 @@ func (cmd *VectorScoreSliceCmd) Clone() Cmder { } } +// VectorScoreSliceSliceCmd is used for VLINKS WITHSCORES which returns an array of arrays. +// In RESP3, each inner array contains maps of element -> score. +type VectorScoreSliceSliceCmd struct { + baseCmd + + val [][]VectorScore +} + +var _ Cmder = (*VectorScoreSliceSliceCmd)(nil) + +func NewVectorScoreSliceSliceCmd(ctx context.Context, args ...any) *VectorScoreSliceSliceCmd { + return &VectorScoreSliceSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *VectorScoreSliceSliceCmd) SetVal(val [][]VectorScore) { + cmd.val = val +} + +func (cmd *VectorScoreSliceSliceCmd) Val() [][]VectorScore { + return cmd.val +} + +func (cmd *VectorScoreSliceSliceCmd) Result() ([][]VectorScore, error) { + return cmd.val, cmd.err +} + +func (cmd *VectorScoreSliceSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *VectorScoreSliceSliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + + cmd.val = make([][]VectorScore, n) + for i := range n { + // Each level can be either a map (RESP3) or an array (RESP2) + levelTyp, err := rd.PeekReplyType() + if err != nil { + return err + } + + if levelTyp == proto.RespMap { + // RESP3 format: each level is a map {element: score, element: score, ...} + mapLen, err := rd.ReadMapLen() + if err != nil { + return err + } + + cmd.val[i] = make([]VectorScore, mapLen) + for j := range mapLen { + name, err := rd.ReadString() + if err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val[i][j] = VectorScore{Name: name, Score: score} + } + } else { + // RESP2 format: each level is an array of [element, score, element, score, ...] pairs + innerLen, err := rd.ReadArrayLen() + if err != nil { + return err + } + + if innerLen%2 != 0 { + return fmt.Errorf("redis: got %d elements in the VLINKS array, wanted a multiple of 2", innerLen) + } + + cmd.val[i] = make([]VectorScore, innerLen/2) + for j := 0; j < innerLen; j += 2 { + name, err := rd.ReadString() + if err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val[i][j/2] = VectorScore{Name: name, Score: score} + } + } + } + + return nil +} + +func (cmd *VectorScoreSliceSliceCmd) Clone() Cmder { + var val [][]VectorScore + if cmd.val != nil { + val = make([][]VectorScore, len(cmd.val)) + for i, slice := range cmd.val { + if slice != nil { + val[i] = make([]VectorScore, len(slice)) + copy(val[i], slice) + } + } + } + return &VectorScoreSliceSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} + +func readVectorAttribStringOrNil(rd *proto.Reader) (*string, error) { + v, err := rd.ReadReply() + if err != nil { + if err == proto.Nil { + return nil, nil + } + return nil, err + } + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("redis: can't parse reply=%T reading string", v) + } + return &s, nil +} + +type VectorAttribSliceCmd struct { + baseCmd + + val []VectorAttrib +} + +var _ Cmder = (*VectorAttribSliceCmd)(nil) + +func NewVectorAttribSliceCmd(ctx context.Context, args ...any) *VectorAttribSliceCmd { + return &VectorAttribSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *VectorAttribSliceCmd) SetVal(val []VectorAttrib) { + cmd.val = val +} + +func (cmd *VectorAttribSliceCmd) Val() []VectorAttrib { + return cmd.val +} + +func (cmd *VectorAttribSliceCmd) Result() ([]VectorAttrib, error) { + return cmd.val, cmd.err +} + +func (cmd *VectorAttribSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *VectorAttribSliceCmd) readReply(rd *proto.Reader) error { + replyType, err := rd.PeekReplyType() + if err != nil { + return err + } + + if replyType == proto.RespMap { + n, err := rd.ReadMapLen() + if err != nil { + return err + } + cmd.val = make([]VectorAttrib, n) + for i := 0; i < n; i++ { + name, err := rd.ReadString() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorAttrib{Name: name, Attribs: attrib} + } + return nil + } + + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + if n%2 != 0 { + return fmt.Errorf("redis: got %d elements in the VSIM array, wanted a multiple of 2", n) + } + cmd.val = make([]VectorAttrib, n/2) + for i := range cmd.val { + name, err := rd.ReadString() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorAttrib{Name: name, Attribs: attrib} + } + return nil +} + +func (cmd *VectorAttribSliceCmd) Clone() Cmder { + return &VectorAttribSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +type VectorScoreAttribSliceCmd struct { + baseCmd + + val []VectorScoreAttrib +} + +var _ Cmder = (*VectorScoreAttribSliceCmd)(nil) + +func NewVectorScoreAttribSliceCmd(ctx context.Context, args ...any) *VectorScoreAttribSliceCmd { + return &VectorScoreAttribSliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + }, + } +} + +func (cmd *VectorScoreAttribSliceCmd) SetVal(val []VectorScoreAttrib) { + cmd.val = val +} + +func (cmd *VectorScoreAttribSliceCmd) Val() []VectorScoreAttrib { + return cmd.val +} + +func (cmd *VectorScoreAttribSliceCmd) Result() ([]VectorScoreAttrib, error) { + return cmd.val, cmd.err +} + +func (cmd *VectorScoreAttribSliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *VectorScoreAttribSliceCmd) readReply(rd *proto.Reader) error { + replyType, err := rd.PeekReplyType() + if err != nil { + return err + } + + if replyType == proto.RespMap { + n, err := rd.ReadMapLen() + if err != nil { + return err + } + cmd.val = make([]VectorScoreAttrib, n) + for i := 0; i < n; i++ { + name, err := rd.ReadString() + if err != nil { + return err + } + if err := rd.ReadFixedArrayLen(2); err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorScoreAttrib{Name: name, Score: score, Attribs: attrib} + } + return nil + } + + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + if n%3 != 0 { + return fmt.Errorf("redis: got %d elements in the VSIM array, wanted a multiple of 3", n) + } + cmd.val = make([]VectorScoreAttrib, n/3) + for i := range cmd.val { + name, err := rd.ReadString() + if err != nil { + return err + } + score, err := rd.ReadFloat() + if err != nil { + return err + } + attrib, err := readVectorAttribStringOrNil(rd) + if err != nil { + return err + } + cmd.val[i] = VectorScoreAttrib{Name: name, Score: score, Attribs: attrib} + } + return nil +} + +func (cmd *VectorScoreAttribSliceCmd) Clone() Cmder { + return &VectorScoreAttribSliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + func (cmd *MonitorCmd) Clone() Cmder { // MonitorCmd cannot be safely cloned due to channels and goroutines // Return a new MonitorCmd with the same channel @@ -7542,6 +8398,13 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) { }); ok { return intCmd.Val(), intCmd.Err() } + case CmdTypeUint: + if uintCmd, ok := cmd.(interface { + Val() uint64 + Err() error + }); ok { + return uintCmd.Val(), uintCmd.Err() + } case CmdTypeBool: if boolCmd, ok := cmd.(interface { Val() bool @@ -7620,6 +8483,14 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) { messages, start := xAutoClaimCmd.Val() return CmdTypeXAutoClaimValue{messages: messages, start: start}, xAutoClaimCmd.Err() } + case CmdTypeXAutoClaimWithDeleted: + if xAutoClaimWithDeletedCmd, ok := cmd.(interface { + Val() ([]XMessage, string, []string) + Err() error + }); ok { + messages, start, deletedIDs := xAutoClaimWithDeletedCmd.Val() + return CmdTypeXAutoClaimWithDeletedValue{messages: messages, start: start, deletedIDs: deletedIDs}, xAutoClaimWithDeletedCmd.Err() + } case CmdTypeXAutoClaimJustID: if xAutoClaimJustIDCmd, ok := cmd.(interface { Val() ([]string, string) @@ -7727,6 +8598,20 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) { }); ok { return hotKeysCmd.Val(), hotKeysCmd.Err() } + case CmdTypeIncrEXInt: + if incrEXCmd, ok := cmd.(interface { + Val() IncrEXIntResult + Err() error + }); ok { + return incrEXCmd.Val(), incrEXCmd.Err() + } + case CmdTypeIncrEXFloat: + if incrEXCmd, ok := cmd.(interface { + Val() IncrEXFloatResult + Err() error + }); ok { + return incrEXCmd.Val(), incrEXCmd.Err() + } case CmdTypeKeyValues: if keyValuesCmd, ok := cmd.(interface { Val() (string, []string) @@ -7946,6 +8831,13 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) { }); ok { return intSliceCmd.Val(), intSliceCmd.Err() } + case CmdTypeUintSlice: + if uintSliceCmd, ok := cmd.(interface { + Val() []uint64 + Err() error + }); ok { + return uintSliceCmd.Val(), uintSliceCmd.Err() + } case CmdTypeBoolSlice: if boolSliceCmd, ok := cmd.(interface { Val() []bool @@ -7974,6 +8866,13 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) { }); ok { return keyValueSliceCmd.Val(), keyValueSliceCmd.Err() } + case CmdTypeAREntrySlice: + if arEntrySliceCmd, ok := cmd.(interface { + Val() []AREntry + Err() error + }); ok { + return arEntrySliceCmd.Val(), arEntrySliceCmd.Err() + } case CmdTypeMapStringString: if mapCmd, ok := cmd.(interface { Val() map[string]string @@ -8025,3 +8924,193 @@ func ExtractCommandValue(cmd interface{}) (interface{}, error) { // If we can't get the command type, return nil return nil, nil } + +//------------------------------------------------------------------------------ + +// IncrEXIntResult is the reply of an INCREX command issued via IncrEXInt. +// Value is the new value of the key; AppliedIncrement is the increment that +// the server actually applied (0 when an out-of-bounds operation was +// rejected, clamped when SATURATE was set). +type IncrEXIntResult struct { + Value int64 + AppliedIncrement int64 +} + +type IncrEXIntCmd struct { + baseCmd + + val IncrEXIntResult +} + +var _ Cmder = (*IncrEXIntCmd)(nil) + +func NewIncrEXIntCmd(ctx context.Context, args ...interface{}) *IncrEXIntCmd { + return &IncrEXIntCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeIncrEXInt, + }, + } +} + +func (cmd *IncrEXIntCmd) SetVal(val IncrEXIntResult) { cmd.val = val } +func (cmd *IncrEXIntCmd) Val() IncrEXIntResult { return cmd.val } +func (cmd *IncrEXIntCmd) Result() (IncrEXIntResult, error) { + return cmd.val, cmd.err +} +func (cmd *IncrEXIntCmd) String() string { return cmdString(cmd, cmd.val) } + +func (cmd *IncrEXIntCmd) readReply(rd *proto.Reader) error { + if err := rd.ReadFixedArrayLen(2); err != nil { + return err + } + value, err := rd.ReadInt() + if err != nil { + return err + } + applied, err := rd.ReadInt() + if err != nil { + return err + } + cmd.val = IncrEXIntResult{Value: value, AppliedIncrement: applied} + return nil +} + +func (cmd *IncrEXIntCmd) Clone() Cmder { + return &IncrEXIntCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +// IncrEXFloatResult is the reply of an INCREX command issued via IncrEXFloat. +type IncrEXFloatResult struct { + Value float64 + AppliedIncrement float64 +} + +type IncrEXFloatCmd struct { + baseCmd + + val IncrEXFloatResult +} + +var _ Cmder = (*IncrEXFloatCmd)(nil) + +func NewIncrEXFloatCmd(ctx context.Context, args ...interface{}) *IncrEXFloatCmd { + return &IncrEXFloatCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeIncrEXFloat, + }, + } +} + +func (cmd *IncrEXFloatCmd) SetVal(val IncrEXFloatResult) { cmd.val = val } +func (cmd *IncrEXFloatCmd) Val() IncrEXFloatResult { return cmd.val } +func (cmd *IncrEXFloatCmd) Result() (IncrEXFloatResult, error) { + return cmd.val, cmd.err +} +func (cmd *IncrEXFloatCmd) String() string { return cmdString(cmd, cmd.val) } + +func (cmd *IncrEXFloatCmd) readReply(rd *proto.Reader) error { + if err := rd.ReadFixedArrayLen(2); err != nil { + return err + } + value, err := rd.ReadFloat() + if err != nil { + return err + } + applied, err := rd.ReadFloat() + if err != nil { + return err + } + cmd.val = IncrEXFloatResult{Value: value, AppliedIncrement: applied} + return nil +} + +func (cmd *IncrEXFloatCmd) Clone() Cmder { + return &IncrEXFloatCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: cmd.val, + } +} + +//------------------------------------------------------------------------------ + +// AREntrySliceCmd is a command that returns index-value pairs from ARSCAN or ARGREP. +type AREntrySliceCmd struct { + baseCmd + val []AREntry +} + +var _ Cmder = (*AREntrySliceCmd)(nil) + +func NewAREntrySliceCmd(ctx context.Context, args ...any) *AREntrySliceCmd { + return &AREntrySliceCmd{ + baseCmd: baseCmd{ + ctx: ctx, + args: args, + cmdType: CmdTypeAREntrySlice, + }, + } +} + +func (cmd *AREntrySliceCmd) SetVal(val []AREntry) { + cmd.val = val +} + +func (cmd *AREntrySliceCmd) Val() []AREntry { + return cmd.val +} + +func (cmd *AREntrySliceCmd) Result() ([]AREntry, error) { + return cmd.val, cmd.err +} + +func (cmd *AREntrySliceCmd) String() string { + return cmdString(cmd, cmd.val) +} + +func (cmd *AREntrySliceCmd) readReply(rd *proto.Reader) error { + n, err := rd.ReadArrayLen() + if err != nil { + return err + } + if n == 0 { + cmd.val = make([]AREntry, 0) + return nil + } + + cmd.val = make([]AREntry, n) + for i := range n { + if err = rd.ReadFixedArrayLen(2); err != nil { + return err + } + + cmd.val[i].Index, err = rd.ReadUint() + if err != nil { + return err + } + + cmd.val[i].Value, err = rd.ReadString() + if err != nil { + return err + } + } + return nil +} + +func (cmd *AREntrySliceCmd) Clone() Cmder { + var val []AREntry + if cmd.val != nil { + val = make([]AREntry, len(cmd.val)) + copy(val, cmd.val) + } + return &AREntrySliceCmd{ + baseCmd: cmd.cloneBaseCmd(), + val: val, + } +} diff --git a/vendor/github.com/redis/go-redis/v9/commands.go b/vendor/github.com/redis/go-redis/v9/commands.go index 219fe464..d347ffeb 100644 --- a/vendor/github.com/redis/go-redis/v9/commands.go +++ b/vendor/github.com/redis/go-redis/v9/commands.go @@ -215,6 +215,7 @@ type Cmdable interface { ShutdownSave(ctx context.Context) *StatusCmd ShutdownNoSave(ctx context.Context) *StatusCmd SlaveOf(ctx context.Context, host, port string) *StatusCmd + ReplicaOf(ctx context.Context, host, port string) *StatusCmd SlowLogGet(ctx context.Context, num int64) *SlowLogCmd SlowLogLen(ctx context.Context) *IntCmd SlowLogReset(ctx context.Context) *StatusCmd @@ -227,6 +228,7 @@ type Cmdable interface { ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd ACLCmdable + ArrayCmdable BitMapCmdable ClusterCmdable GenericCmdable @@ -448,6 +450,20 @@ func (c cmdable) Do(ctx context.Context, args ...interface{}) *Cmd { return cmd } +// DoRaw executes a command and returns the raw RESP protocol bytes without parsing. +func (c cmdable) DoRaw(ctx context.Context, args ...interface{}) *RawCmd { + cmd := NewRawCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// DoRawWriteTo executes a command and streams raw RESP bytes directly to w without intermediate allocations. +func (c cmdable) DoRawWriteTo(ctx context.Context, w io.Writer, args ...interface{}) *RawWriteToCmd { + cmd := NewRawWriteToCmd(ctx, w, args...) + _ = c(ctx, cmd) + return cmd +} + // Quit closes the connection. // // Deprecated: Just close the connection instead as of Redis 7.2.0. @@ -682,6 +698,13 @@ func (c cmdable) SlaveOf(ctx context.Context, host, port string) *StatusCmd { return cmd } +// ReplicaOf sets a Redis server as a replica of another, or promotes it to being a master. +func (c cmdable) ReplicaOf(ctx context.Context, host, port string) *StatusCmd { + cmd := NewStatusCmd(ctx, "replicaof", host, port) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) SlowLogGet(ctx context.Context, num int64) *SlowLogCmd { cmd := NewSlowLogCmd(context.Background(), "slowlog", "get", num) _ = c(ctx, cmd) diff --git a/vendor/github.com/redis/go-redis/v9/dial_retry_backoff.go b/vendor/github.com/redis/go-redis/v9/dial_retry_backoff.go new file mode 100644 index 00000000..bb3e8bf2 --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/dial_retry_backoff.go @@ -0,0 +1,39 @@ +package redis + +import ( + "time" + + "github.com/redis/go-redis/v9/internal" +) + +// DialRetryBackoffConstant returns a dial retry backoff function that always returns d. +// attempt is 0-based: attempt=0 is the delay after the 1st failed dial. +func DialRetryBackoffConstant(d time.Duration) func(attempt int) time.Duration { + if d < 0 { + d = 0 + } + return func(int) time.Duration { return d } +} + +// DialRetryBackoffExponential returns a dial retry backoff function that uses exponential +// backoff with jitter and a cap, using internal.RetryBackoff. +// +// attempt is 0-based: attempt=0 is the delay after the 1st failed dial. +func DialRetryBackoffExponential(minBackoff, maxBackoff time.Duration) func(attempt int) time.Duration { + if minBackoff < 0 { + minBackoff = 0 + } + if maxBackoff < 0 { + maxBackoff = 0 + } + if minBackoff > maxBackoff { + minBackoff = maxBackoff + } + return func(attempt int) time.Duration { + // internal.RetryBackoff expects retry >= 0. + if attempt < 0 { + attempt = 0 + } + return internal.RetryBackoff(attempt, minBackoff, maxBackoff) + } +} diff --git a/vendor/github.com/redis/go-redis/v9/docker-compose.yml b/vendor/github.com/redis/go-redis/v9/docker-compose.yml index 8299fd9d..fed908be 100644 --- a/vendor/github.com/redis/go-redis/v9/docker-compose.yml +++ b/vendor/github.com/redis/go-redis/v9/docker-compose.yml @@ -1,6 +1,6 @@ --- -x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.6.0} +x-default-image: &default-image ${CLIENT_LIBS_TEST_IMAGE:-redislabs/client-libs-test:8.8.0} services: redis: @@ -164,9 +164,9 @@ services: - PORT=6390 command: ${REDIS_EXTRA_ARGS:---enable-debug-command yes --enable-module-command yes --tls-auth-clients optional --save ""} ports: - - 6390:6390 - - 6391:6391 - - 6392:6392 + - "6390:6390" + - "6391:6391" + - "6392:6392" volumes: - "./dockers/ring:/redis/work" profiles: diff --git a/vendor/github.com/redis/go-redis/v9/error.go b/vendor/github.com/redis/go-redis/v9/error.go index d2462a49..06ecca74 100644 --- a/vendor/github.com/redis/go-redis/v9/error.go +++ b/vendor/github.com/redis/go-redis/v9/error.go @@ -28,6 +28,11 @@ var ErrPoolTimeout = pool.ErrPoolTimeout // is used on a ClusterClient with keys in different slots. var ErrCrossSlot = proto.RedisError("CROSSSLOT Keys in request don't hash to the same slot") +// ErrNoScript is returned when EVALSHA is requested for a script digest that +// is not available in the script cache. Note that this error text is reproduced +// literally from that used by Redis. +var ErrNoScript = proto.RedisError("NOSCRIPT No matching script. Please use EVAL.") + // HasErrorPrefix checks if the err is a Redis error and the message contains a prefix. func HasErrorPrefix(err error, prefix string) bool { var rErr Error @@ -86,6 +91,14 @@ func shouldRetry(err error, retryTimeout bool) bool { return true } + // Dial errors mean TCP connection was never established β€” safe to retry even + // when wrapped inside context.DeadlineExceeded (from DialTimeout context). + // Must be checked before the context error check below. + var opErr *net.OpError + if errors.As(err, &opErr) && opErr.Op == "dial" { + return true + } + // Check for context errors (works with wrapped errors) if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return false @@ -139,6 +152,9 @@ func shouldRetry(err error, retryTimeout bool) bool { if strings.HasPrefix(s, "READONLY ") { return true } + if strings.Contains(s, "-READONLY You can't write against a read only replica") { + return true + } if strings.HasPrefix(s, "CLUSTERDOWN ") { return true } diff --git a/vendor/github.com/redis/go-redis/v9/hash_commands.go b/vendor/github.com/redis/go-redis/v9/hash_commands.go index b78860a5..256b8746 100644 --- a/vendor/github.com/redis/go-redis/v9/hash_commands.go +++ b/vendor/github.com/redis/go-redis/v9/hash_commands.go @@ -70,6 +70,13 @@ func (c cmdable) HGet(ctx context.Context, key, field string) *StringCmd { return cmd } +// HGetAll returns a map of all fields and values stored at key. +// +// Returns an empty map when key does not exist. +// +// Time complexity: O(N) where N is the size of the hash. +// +// See https://redis.io/commands/hgetall/ func (c cmdable) HGetAll(ctx context.Context, key string) *MapStringStringCmd { cmd := NewMapStringStringCmd(ctx, "hgetall", key) _ = c(ctx, cmd) diff --git a/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go b/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go index ea56fd6c..8aa87db3 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go +++ b/vendor/github.com/redis/go-redis/v9/internal/hashtag/hashtag.go @@ -1,9 +1,8 @@ package hashtag import ( + "math/rand" "strings" - - "github.com/redis/go-redis/v9/internal/rand" ) const slotNumber = 16384 @@ -11,7 +10,7 @@ const slotNumber = 16384 // CRC16 implementation according to CCITT standards. // Copyright 2001-2010 Georges Menie (www.menie.org) // Copyright 2013 The Go Authors. All rights reserved. -// http://redis.io/topics/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c +// https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c. var crc16tab = [256]uint16{ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, diff --git a/vendor/github.com/redis/go-redis/v9/internal/hashtag/rendezvous.go b/vendor/github.com/redis/go-redis/v9/internal/hashtag/rendezvous.go new file mode 100644 index 00000000..214f7e8e --- /dev/null +++ b/vendor/github.com/redis/go-redis/v9/internal/hashtag/rendezvous.go @@ -0,0 +1,54 @@ +package hashtag + +import "github.com/cespare/xxhash/v2" + +// RendezvousHash implements HRW (Highest Random Weight) hashing. +type RendezvousHash struct { + nodes []node +} + +type node struct { + name string + hash uint64 +} + +// NewRendezvousHash builds a hash from shard names. +func NewRendezvousHash(shards []string) *RendezvousHash { + n := make([]node, len(shards)) + for i, s := range shards { + n[i] = node{ + name: s, + hash: xxhash.Sum64String(s), + } + } + return &RendezvousHash{nodes: n} +} + +// Get returns the shard name for the given key. +func (r *RendezvousHash) Get(key string) string { + if len(r.nodes) == 0 { + return "" + } + + kh := xxhash.Sum64String(key) + + bestIdx := 0 + bestScore := mix64(kh ^ r.nodes[0].hash) + + for i := 1; i < len(r.nodes); i++ { + if score := mix64(kh ^ r.nodes[i].hash); score > bestScore { + bestScore = score + bestIdx = i + } + } + + return r.nodes[bestIdx].name +} + +// mix64 is a xorshift-based mixing function. +func mix64(x uint64) uint64 { + x ^= x >> 12 + x ^= x << 25 + x ^= x >> 27 + return x * 2685821657736338717 +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go b/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go index 1a560e4a..408ce0e4 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go +++ b/vendor/github.com/redis/go-redis/v9/internal/hscan/structmap.go @@ -109,6 +109,8 @@ func (s StructValue) Scan(key string, value string) error { return scan.ScanRedis(value) case encoding.TextUnmarshaler: return scan.UnmarshalText(util.StringToBytes(value)) + case encoding.BinaryUnmarshaler: + return scan.UnmarshalBinary(util.StringToBytes(value)) } } diff --git a/vendor/github.com/redis/go-redis/v9/internal/internal.go b/vendor/github.com/redis/go-redis/v9/internal/internal.go index e783d139..403db56c 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/internal.go +++ b/vendor/github.com/redis/go-redis/v9/internal/internal.go @@ -1,9 +1,8 @@ package internal import ( + "math/rand" "time" - - "github.com/redis/go-redis/v9/internal/rand" ) func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration { diff --git a/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go b/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go index a4840825..a3f23fff 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go +++ b/vendor/github.com/redis/go-redis/v9/internal/otel/metrics.go @@ -85,6 +85,17 @@ type Recorder interface { // consumerGroup: name of the consumer group // consumerName: name of the consumer RecordStreamLag(ctx context.Context, lag time.Duration, cn *pool.Conn, streamName, consumerGroup, consumerName string) + + // RecordConnectionCount records a change in connection count (UpDownCounter) + // delta: +1 when connection added, -1 when connection removed + // state: connection state (e.g., "idle", "used") + // isPubSub: true if this is a PubSub connection + RecordConnectionCount(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) + + // RecordPendingRequests records a change in pending requests (UpDownCounter) + // delta: +1 when request starts waiting, -1 when request stops waiting + // poolName is passed explicitly because we may not have a connection yet when request starts + RecordPendingRequests(ctx context.Context, delta int, cn *pool.Conn, poolName string) } type PubSubPooler interface { @@ -193,6 +204,12 @@ func SetGlobalRecorder(r Recorder) { ConnectionClosed: func(ctx context.Context, cn *pool.Conn, reason string, err error) { getRecorder().RecordConnectionClosed(ctx, cn, reason, err) }, + ConnectionCount: func(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) { + getRecorder().RecordConnectionCount(ctx, delta, cn, state, isPubSub) + }, + PendingRequests: func(ctx context.Context, delta int, cn *pool.Conn, poolName string) { + getRecorder().RecordPendingRequests(ctx, delta, cn, poolName) + }, }) } @@ -246,6 +263,8 @@ func (noopRecorder) RecordPubSubMessage(context.Context, *pool.Conn, string, str func (noopRecorder) RecordStreamLag(context.Context, time.Duration, *pool.Conn, string, string, string) { } +func (noopRecorder) RecordConnectionCount(context.Context, int, *pool.Conn, string, bool) {} +func (noopRecorder) RecordPendingRequests(context.Context, int, *pool.Conn, string) {} // RegisterPools registers connection pools with the global recorder. func RegisterPools(connPool pool.Pooler, pubSubPool PubSubPooler, addr string) { diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go b/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go index f0af63c6..fab54654 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/conn.go @@ -14,6 +14,7 @@ import ( "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/maintnotifications/logs" "github.com/redis/go-redis/v9/internal/proto" + uberatomic "go.uber.org/atomic" ) var noDeadline = time.Time{} @@ -81,8 +82,10 @@ type Conn struct { bw *bufio.Writer wr *proto.Writer - // Lightweight mutex to protect reader operations during handoff - // Only used for the brief period during SetNetConn and HasBufferedData/PeekReplyTypeSafe + // Lightweight mutex to protect reader operations during handoff and health checks + // Used during: + // - SetNetConn (write lock for resetting reader state) + // - HasBufferedData/PeekReplyTypeSafe (read lock for safe concurrent peek operations) readerMu sync.RWMutex // State machine for connection state management @@ -102,11 +105,15 @@ type Conn struct { pooled bool pubsub bool - closed atomic.Bool createdAt time.Time expiresAt time.Time poolName string // Name of the pool this connection belongs to (for metrics) + // When a goroutine closes a connection, it usually knows the reason, so closeReason is not needed. + // closeReason is only used when an in-use connection is closed by another goroutine, + // to inform the goroutine using the connection why the connection was closed. + closeReason uberatomic.String + // maintenanceNotifications upgrade support: relaxed timeouts during migrations/failovers // Using atomic operations for lock-free access to avoid mutex contention @@ -576,6 +583,41 @@ func (cn *Conn) getEffectiveWriteTimeout(normalTimeout time.Duration) time.Durat } } +// SetOnClose installs fn as the callback invoked exactly once when this +// connection is closed (via Conn.Close). +// +// IMPORTANT: SetOnClose OVERWRITES any previously installed callback β€” it +// does not compose, chain, or deduplicate. A Conn has room for a single +// onClose hook by design, because its lifecycle is bounded (a Conn is +// created, optionally re-initialized on its own net.Conn, and then closed +// once) and the pool's OnRemove hooks handle any registry-level cleanup +// that must survive the net.Conn being swapped. +// +// This has a subtle implication for per-connection subscriptions such as +// the unsubscribe function returned by StreamingCredentialsProvider +// (e.g. EntraID token rotation): if SetOnClose is called twice on the +// same Conn with DIFFERENT unsubscribe closures β€” for example because +// initConn ran a second time and obtained a fresh Subscribe() β€” +// the previous unsubscribe is dropped and will NEVER run, leaking a +// subscription on the provider. Callers must therefore ensure either: +// +// - the provider's Subscribe is idempotent for the same listener (the +// streaming credentials Manager deduplicates listeners by connection +// id, so re-Subscribe returns an equivalent unsubscribe), OR +// - the previous callback has already been invoked before SetOnClose is +// called again. +// +// Design note: unlike the client-level onCloseHooks registry (see +// redis.baseClient), there is intentionally NO named-hook dedup or +// multi-callback support on Conn. This is a deliberate trade-off to keep +// the Conn object slim β€” a pool can hold thousands of Conn values and +// each one is a hot allocation, so paying for a sync.Mutex plus a +// map[string]func() error per connection to support a feature that would +// only be used by at most one subsystem today (streaming credentials) is +// not worth the per-connection memory and allocation cost. For a single +// Conn there is at most one meaningful close callback at any point in +// time, and a richer registry here would not even solve the "stale +// closure" hazard described above. func (cn *Conn) SetOnClose(fn func() error) { cn.onClose = fn } @@ -882,18 +924,20 @@ func (cn *Conn) WithWriter( } func (cn *Conn) IsClosed() bool { - return cn.closed.Load() || cn.stateMachine.GetState() == StateClosed + return cn.stateMachine.GetState() == StateClosed } func (cn *Conn) Close() error { - cn.closed.Store(true) - + if cn.IsClosed() { + return nil + } // Transition to CLOSED state cn.stateMachine.Transition(StateClosed) if cn.onClose != nil { // ignore error _ = cn.onClose() + cn.onClose = nil } // Lock-free netConn access for better performance diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go index afdc631c..7dee4c49 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/conn_state.go @@ -297,45 +297,38 @@ func (sm *ConnStateMachine) notifyWaiters() { return } - // Process waiters in FIFO order until no more can be processed - // We loop instead of recursing to avoid stack overflow and mutex issues + // Track state locally so we only consider transitions made within this + // call, not concurrent transitions from woken goroutines. Re-reading the + // atomic would let a fast goroutine's Transition(StateIdle) leak into our + // view, causing us to wake multiple waiters at once and breaking FIFO + // execution ordering. + currentState := sm.GetState() + for { processed := false - // Find the first waiter that can proceed for elem := sm.waiters.Front(); elem != nil; elem = elem.Next() { w := elem.Value.(*waiter) - // Read current state inside the loop to get the latest value - currentState := sm.GetState() - - // Check if current state is valid for this waiter if _, valid := w.validStates[currentState]; valid { - // Remove from queue first sm.waiters.Remove(elem) sm.waiterCount.Add(-1) - // Use CAS to ensure state hasn't changed since we checked - // This prevents race condition where another thread changes state - // between our check and our transition if sm.state.CompareAndSwap(uint32(currentState), uint32(w.targetState)) { - // Successfully transitioned - notify waiter w.done <- nil + currentState = w.targetState processed = true break } else { - // State changed - re-add waiter to front of queue to maintain FIFO ordering - // This waiter was first in line and should retain priority sm.waiters.PushFront(w) sm.waiterCount.Add(1) - // Continue to next iteration to re-read state + currentState = sm.GetState() processed = true break } } } - // If we didn't process any waiter, we're done if !processed { break } diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go index aaca530c..d551fbb1 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pool.go @@ -3,6 +3,7 @@ package pool import ( "context" "errors" + "math/rand" "net" "sync" "sync/atomic" @@ -10,7 +11,41 @@ import ( "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/proto" - "github.com/redis/go-redis/v9/internal/rand" +) + +// Connection close reason constants for metrics. +// These are used as the "reason" parameter in CloseConn() calls. +const ( + // CloseReasonStale indicates the connection was closed because it exceeded + // the idle timeout or max lifetime. + CloseReasonStale = "stale" + + // CloseReasonHookError indicates the connection was closed due to an error + // in a pool hook (OnGet or OnPut). + CloseReasonHookError = "hook_error" + + // CloseReasonAuthError indicates the connection was closed due to an + // authentication error during re-authentication. + CloseReasonAuthError = "auth_error" + + // CloseReasonTest is used in tests when closing connections. + CloseReasonTest = "test" + + // CloseReasonFailover indicates the connection was closed due to a failover event. + CloseReasonFailover = "failover" +) + +// Metric state constants for connection state tracking. +// These represent the logical state of a connection from a metrics perspective, +// not the internal state machine state (ConnState). +const ( + // MetricStateIdle indicates the connection is idle in the pool, + // ready to be acquired. + MetricStateIdle = "idle" + + // MetricStateUsed indicates the connection is currently being used + // by a client operation. + MetricStateUsed = "used" ) var ( @@ -69,6 +104,15 @@ var ( // Parameters: ctx, cn, reason, err metricConnectionClosedCallback func(ctx context.Context, cn *Conn, reason string, err error) + // Global metric callback for connection count changes (UpDownCounter) + // Parameters: ctx, delta (+1/-1), cn, state, isPubSub + metricConnectionCountCallback func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool) + + // Global metric callback for pending requests changes (UpDownCounter) + // Parameters: ctx, delta (+1/-1), cn, poolName + // poolName is passed explicitly because we may not have a connection yet when request starts + metricPendingRequestsCallback func(ctx context.Context, delta int, cn *Conn, poolName string) + // errPanicInDial is returned when a panic occurs in the dial function. errPanicInQueuedNewConn = errors.New("panic in queuedNewConn") @@ -114,6 +158,17 @@ type MetricCallbacks struct { // ConnectionClosed is called when a connection is closed ConnectionClosed func(ctx context.Context, cn *Conn, reason string, err error) + + // ConnectionCount is called when connection count changes (UpDownCounter) + // delta: +1 when connection added, -1 when connection removed + // state: connection state (e.g., "idle", "used") + // isPubSub: true if this is a PubSub connection + ConnectionCount func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool) + + // PendingRequests is called when pending requests count changes (UpDownCounter) + // delta: +1 when request starts waiting, -1 when request stops waiting + // poolName is passed explicitly because we may not have a connection yet when request starts + PendingRequests func(ctx context.Context, delta int, cn *Conn, poolName string) } // SetAllMetricCallbacks sets all metric callbacks atomically. @@ -138,6 +193,8 @@ func SetAllMetricCallbacks(callbacks *MetricCallbacks) { metricMaintenanceNotificationCallback = nil metricConnectionWaitTimeCallback = nil metricConnectionClosedCallback = nil + metricConnectionCountCallback = nil + metricPendingRequestsCallback = nil return } @@ -148,6 +205,8 @@ func SetAllMetricCallbacks(callbacks *MetricCallbacks) { metricMaintenanceNotificationCallback = callbacks.MaintenanceNotification metricConnectionWaitTimeCallback = callbacks.ConnectionWaitTime metricConnectionClosedCallback = callbacks.ConnectionClosed + metricConnectionCountCallback = callbacks.ConnectionCount + metricPendingRequestsCallback = callbacks.PendingRequests } // getMetricConnectionStateChangeCallback returns the metric callback for connection state changes. @@ -223,6 +282,22 @@ func getMetricConnectionClosedCallback() func(ctx context.Context, cn *Conn, rea return cb } +// getMetricConnectionCountCallback returns the metric callback for connection count changes (UpDownCounter). +func getMetricConnectionCountCallback() func(ctx context.Context, delta int, cn *Conn, state string, isPubSub bool) { + metricCallbackMu.RLock() + cb := metricConnectionCountCallback + metricCallbackMu.RUnlock() + return cb +} + +// getMetricPendingRequestsCallback returns the metric callback for pending requests changes (UpDownCounter). +func getMetricPendingRequestsCallback() func(ctx context.Context, delta int, cn *Conn, poolName string) { + metricCallbackMu.RLock() + cb := metricPendingRequestsCallback + metricCallbackMu.RUnlock() + return cb +} + // Stats contains pool state information and accumulated stats. type Stats struct { Hits uint32 // number of times free connection was found in the pool @@ -242,7 +317,7 @@ type Stats struct { type Pooler interface { NewConn(context.Context) (*Conn, error) - CloseConn(*Conn) error + CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error Get(context.Context) (*Conn, error) Put(context.Context, *Conn) @@ -294,6 +369,10 @@ type Options struct { // Default: 100ms DialerRetryTimeout time.Duration + // DialerRetryBackoff controls the delay between dial retry attempts. + // If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms). + DialerRetryBackoff func(attempt int) time.Duration + // Name is a unique identifier for this pool, used in metrics. // Format: addr_uniqueID (e.g., "localhost:6379_a1b2c3d4") Name string @@ -309,7 +388,6 @@ type ConnPool struct { dialErrorsNum uint32 // atomic lastDialError atomic.Value - queue chan struct{} dialsInProgress chan struct{} dialsQueue *wantConnQueue // Fast semaphore for connection limiting with eventual fairness @@ -341,7 +419,6 @@ func NewConnPool(opt *Options) *ConnPool { p := &ConnPool{ cfg: opt, semaphore: internal.NewFastSemaphore(opt.PoolSize), - queue: make(chan struct{}, opt.PoolSize), conns: make(map[uint64]*Conn), dialsInProgress: make(chan struct{}, opt.MaxConcurrentDials), dialsQueue: newWantConnQueue(), @@ -456,10 +533,8 @@ func (p *ConnPool) checkMinIdleConns() { } func (p *ConnPool) addIdleConn() error { - ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) - defer cancel() - - cn, err := p.dialConn(ctx, true) + // Do not apply DialTimeout via context here; dialConn applies DialTimeout per attempt. + cn, err := p.dialConn(context.Background(), true) if err != nil { return err } @@ -479,6 +554,12 @@ func (p *ConnPool) addIdleConn() error { p.conns[cn.GetID()] = cn p.idleConns = append(p.idleConns, cn) + + // Record connection count increment (new idle connection from min-idle prewarm) + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(context.Background(), 1, cn, "idle", false) + } + return nil } @@ -505,9 +586,9 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) { ctx = context.Background() } - dialCtx, cancel := context.WithTimeout(ctx, p.cfg.DialTimeout) - defer cancel() - cn, err := p.dialConn(dialCtx, pooled) + // Do not apply DialTimeout via context here; dialConn applies DialTimeout per attempt. + // We still propagate ctx so callers can cancel explicitly. + cn, err := p.dialConn(ctx, pooled) if err != nil { return nil, err } @@ -543,9 +624,14 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) { } } - // Notify metrics: new connection created and idle + // All new connections start as "used" metrically. For the miss path in getConn, + // this is the final state. For putIdleConn (undelivered conn), a usedβ†’idle + // transition is emitted when it's added to idleConns. if cb := getMetricConnectionStateChangeCallback(); cb != nil { - cb(ctx, cn, "", "idle") + cb(ctx, cn, "", MetricStateUsed) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, 1, cn, "used", false) } return cn, nil @@ -569,16 +655,12 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) { } // Retry dialing with backoff - // the context timeout is already handled by the context passed in - // so we may never reach the max retries, higher values don't hurt + // Dial timeout is applied per attempt (so retries/backoff don't eat into the next + // attempt's dial budget), while still honoring caller cancellation via ctx. maxRetries := p.cfg.DialerRetries if maxRetries <= 0 { maxRetries = 5 // Default value } - backoffDuration := p.cfg.DialerRetryTimeout - if backoffDuration <= 0 { - backoffDuration = 100 * time.Millisecond // Default value - } var lastErr error shouldLoop := true @@ -587,16 +669,32 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) { // instead of a generic context deadline exceeded error attempt := 0 for attempt = 0; (attempt < maxRetries) && shouldLoop; attempt++ { - netConn, err := p.cfg.Dialer(ctx) + attemptCtx := ctx + var cancel context.CancelFunc + if p.cfg.DialTimeout > 0 { + // Apply DialTimeout per attempt, but never extend an existing earlier deadline. + if deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) > p.cfg.DialTimeout { + attemptCtx, cancel = context.WithTimeout(ctx, p.cfg.DialTimeout) + } + } + + netConn, err := p.cfg.Dialer(attemptCtx) + if cancel != nil { + cancel() + } if err != nil { lastErr = err // Add backoff delay for retry attempts // (not for the first attempt, do at least one) - select { - case <-ctx.Done(): - shouldLoop = false - case <-time.After(backoffDuration): - // Continue with retry + // Do not sleep after the last attempt. + if attempt+1 < maxRetries { + backoffDuration := p.dialRetryBackoff(attempt) + select { + case <-ctx.Done(): + shouldLoop = false + case <-time.After(backoffDuration): + // Continue with retry + } } continue } @@ -623,6 +721,22 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) { return nil, lastErr } +func (p *ConnPool) dialRetryBackoff(attempt int) time.Duration { + if p.cfg.DialerRetryBackoff != nil { + d := p.cfg.DialerRetryBackoff(attempt) + if d < 0 { + return 0 + } + return d + } + + base := p.cfg.DialerRetryTimeout + if base <= 0 { + base = 100 * time.Millisecond + } + return base +} + // calcConnExpiresAt calculates the expiration time for a connection. // It applies random jitter to prevent all connections from expiring simultaneously, // avoiding the "thundering herd" problem where all connections expire at once. @@ -648,19 +762,26 @@ func (p *ConnPool) tryDial() { return } - ctx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) + // Probe dialing even when dialErrorsNum is saturated. Apply DialTimeout per probe + // attempt so custom dialers can't hang indefinitely. + ctx := context.Background() + var cancel context.CancelFunc + if p.cfg.DialTimeout > 0 { + ctx, cancel = context.WithTimeout(ctx, p.cfg.DialTimeout) + } conn, err := p.cfg.Dialer(ctx) + if cancel != nil { + cancel() + } if err != nil { p.setLastDialError(err) time.Sleep(time.Second) - cancel() continue } atomic.StoreUint32(&p.dialErrorsNum, 0) _ = conn.Close() - cancel() return } } @@ -689,12 +810,21 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) { } // Track pending requests in pool stats - // NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly. atomic.AddUint32(&p.stats.PendingRequests, 1) + // Record pending request increment (UpDownCounter) + // Pass pool name explicitly since we don't have a connection yet + poolName := p.cfg.Name + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, 1, nil, poolName) + } defer func() { if err != nil { // Failed to get connection, decrement pending requests atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1 + // Record pending request decrement on failure + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, -1, nil, poolName) + } } }() @@ -732,6 +862,17 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) { p.connsMu.Lock() cn, err = p.popIdle() + if cn != nil { + // Emit idleβ†’used transition inside the lock so Close() sees + // consistent state (conn removed from idleConns = "used"). + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateIdle, MetricStateUsed) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "idle", false) + cb(ctx, 1, cn, "used", false) + } + } p.connsMu.Unlock() if err != nil { @@ -744,7 +885,8 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) { } if !p.isHealthyConn(cn, nowNs) { - _ = p.CloseConn(cn) + // Connection was already transitioned to MetricStateUsed under the lock above. + _ = p.CloseConn(ctx, cn, CloseReasonStale, MetricStateUsed) continue } @@ -755,11 +897,13 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) { if hookErr != nil || !acceptConn { if hookErr != nil { internal.Logger.Printf(ctx, "redis: connection pool: failed to process idle connection by hook: %v", hookErr) - _ = p.CloseConn(cn) + // Connection was already transitioned to MetricStateUsed under the lock above. + _ = p.CloseConn(ctx, cn, CloseReasonHookError, MetricStateUsed) } else { internal.Logger.Printf(ctx, "redis: connection pool: conn[%d] rejected by hook, returning to pool", cn.GetID()) + // Connection is already in MetricStateUsed (transitioned under the lock above). // Return connection to pool without freeing the turn that this Get() call holds. - // We use putConnWithoutTurn() to run all the Put hooks and logic without freeing a turn. + // putConnWithoutTurn will emit usedβ†’idle transition. p.putConnWithoutTurn(ctx, cn) cn = nil } @@ -769,19 +913,17 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) { atomic.AddUint32(&p.stats.Hits, 1) - // Notify metrics: connection moved from idle to used - if cb := getMetricConnectionStateChangeCallback(); cb != nil { - cb(ctx, cn, "idle", "used") - } - // Record wait time (use cached callback from above) if waitTimeCallback != nil { waitTimeCallback(ctx, waitDuration, cn) } // Decrement pending requests (connection acquired successfully) - // NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly. atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1 + // Record pending request decrement (UpDownCounter) + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, -1, cn, poolName) + } return cn, nil } @@ -802,26 +944,38 @@ func (p *ConnPool) getConn(ctx context.Context) (cn *Conn, err error) { // both errors and accept=false mean a hook rejected the connection // this should not happen with a new connection, but we handle it gracefully if err != nil || !acceptConn { - // Failed to process connection, discard it internal.Logger.Printf(ctx, "redis: connection pool: failed to process new connection conn[%d] by hook: accept=%v, err=%v", newcn.GetID(), acceptConn, err) - _ = p.CloseConn(newcn) + // newConn emitted +1 used; CloseConn will emit -1 used if we own the removal. + _ = p.CloseConn(ctx, newcn, CloseReasonHookError, MetricStateUsed) return nil, err } - } - // Notify metrics: new connection is created and used - if cb := getMetricConnectionStateChangeCallback(); cb != nil { - cb(ctx, newcn, "", "used") + // Record connection creation time metric when hooks are used. + // When hookManager is set, ProcessOnGet initializes the connection (AUTH/HELLO), + // causing IsInited()=true. This means _getConn() in redis.go will take the + // early return path and never reach its create time recording. + // When hookManager is nil, _getConn() handles both initialization and create time recording. + if dialStartNs := newcn.GetDialStartNs(); dialStartNs > 0 { + if cb := GetMetricConnectionCreateTimeCallback(); cb != nil { + duration := time.Duration(time.Now().UnixNano() - dialStartNs) + cb(ctx, duration, newcn) + } + } } + // newConn already emitted +1 used, so no transition needed here. + // Record wait time (use cached callback from above) if waitTimeCallback != nil { waitTimeCallback(ctx, waitDuration, newcn) } // Decrement pending requests (connection acquired successfully) - // NOTE: We only track in stats, not via callback. The AsyncGauge reads stats directly. atomic.AddUint32(&p.stats.PendingRequests, ^uint32(0)) // -1 + // Record pending request decrement (UpDownCounter) + if cb := getMetricPendingRequestsCallback(); cb != nil { + cb(ctx, -1, newcn, poolName) + } return newcn, nil } @@ -835,7 +989,8 @@ func (p *ConnPool) queuedNewConn(ctx context.Context) (*Conn, error) { return nil, ctx.Err() } - dialCtx, cancel := context.WithTimeout(context.Background(), p.cfg.DialTimeout) + // Don't apply DialTimeout via context here; dialConn applies DialTimeout per attempt. + dialCtx, cancel := context.WithCancel(context.Background()) w := &wantConn{ ctx: dialCtx, @@ -919,14 +1074,24 @@ func (p *ConnPool) putIdleConn(ctx context.Context, cn *Conn) bool { defer p.connsMu.Unlock() if p.closed() { - _ = cn.Close() + // Don't close here β€” this connection is still in p.conns and Close() + // will handle closing it and emitting the correct metric decrements. + // We just skip adding it to idleConns. return true } - // poolSize is increased in newConn p.idleConns = append(p.idleConns, cn) p.idleConnsLen.Add(1) + // Connection was created as "used" in newConn; transition to idle. + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, MetricStateIdle) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + cb(ctx, 1, cn, "idle", false) + } + return true } @@ -1087,6 +1252,7 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) { } var shouldCloseConn bool + var removedFromPool bool if p.cfg.MaxIdleConns == 0 || p.idleConnsLen.Load() < p.cfg.MaxIdleConns { // Hot path optimization: try fast IN_USE β†’ IDLE transition @@ -1111,7 +1277,7 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) { case StateClosed: internal.Logger.Printf(ctx, "Unexpected conn[%d] state changed by hook to %v, closing it", cn.GetID(), currentState) shouldCloseConn = true - p.removeConnWithLock(cn) + removedFromPool = p.removeConnWithLock(cn) default: // Pool as-is internal.Logger.Printf(ctx, "Unexpected conn[%d] state changed by hook to %v, pooling as-is", cn.GetID(), currentState) @@ -1122,34 +1288,73 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) { // put them at the opposite end of the queue // Optimization: if we just transitioned to IDLE, we know it's usable - skip the check if !transitionedToIdle && !cn.IsUsable() { - if p.cfg.PoolFIFO { - p.connsMu.Lock() - p.idleConns = append(p.idleConns, cn) + p.connsMu.Lock() + // Check if Close() already removed this connection from p.conns. + // If so, skip the append and metrics β€” Close() already accounted for it. + if _, inPool := p.conns[cn.GetID()]; inPool { + if p.cfg.PoolFIFO { + p.idleConns = append(p.idleConns, cn) + } else { + p.idleConns = append([]*Conn{cn}, p.idleConns...) + } + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, MetricStateIdle) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + cb(ctx, 1, cn, "idle", false) + } p.connsMu.Unlock() + p.idleConnsLen.Add(1) } else { - p.connsMu.Lock() - p.idleConns = append([]*Conn{cn}, p.idleConns...) + shouldCloseConn = true p.connsMu.Unlock() } - p.idleConnsLen.Add(1) } else if !shouldCloseConn { p.connsMu.Lock() - p.idleConns = append(p.idleConns, cn) - p.connsMu.Unlock() - p.idleConnsLen.Add(1) + if _, inPool := p.conns[cn.GetID()]; inPool { + p.idleConns = append(p.idleConns, cn) + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, MetricStateIdle) + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + cb(ctx, 1, cn, "idle", false) + } + p.connsMu.Unlock() + p.idleConnsLen.Add(1) + } else { + shouldCloseConn = true + p.connsMu.Unlock() + } } - // Notify metrics: connection moved from used to idle - if cb := getMetricConnectionStateChangeCallback(); cb != nil { - cb(ctx, cn, "used", "idle") + if shouldCloseConn { + // Connection was removed (e.g., hook set state to StateClosed). + // Only emit if we actually removed it from the map (not already taken by Close()). + if removedFromPool { + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, "") + } + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + } + } } } else { shouldCloseConn = true - p.removeConnWithLock(cn) + removedFromPool = p.removeConnWithLock(cn) - // Notify metrics: connection removed (used -> nothing) - if cb := getMetricConnectionStateChangeCallback(); cb != nil { - cb(ctx, cn, "used", "") + // Only emit if we actually removed it from the map (not already taken by Close()). + if removedFromPool { + // Notify metrics: connection removed (used -> nothing) + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, "") + } + // Record connection count decrement (connection removed while in used state) + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + } } } @@ -1158,6 +1363,17 @@ func (p *ConnPool) putConn(ctx context.Context, cn *Conn, freeTurn bool) { } if shouldCloseConn { + // Only emit connection closed if we actually owned the removal. + // If removedFromPool is false, Close() already emitted connectionClosed for this conn. + if removedFromPool { + if cb := getMetricConnectionClosedCallback(); cb != nil { + reason := "conn_pool_close" + if r := cn.closeReason.Load(); r != "" { + reason = r + } + cb(ctx, cn, reason, nil) + } + } _ = p.closeConn(cn) } @@ -1185,24 +1401,35 @@ func (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason erro hookManager.ProcessOnRemove(ctx, cn, reason) } - p.removeConnWithLock(cn) + removed := p.removeConnWithLock(cn) if freeTurn { p.freeTurn() } - // Notify metrics: connection removed (assume from used state) - if cb := getMetricConnectionStateChangeCallback(); cb != nil { - cb(ctx, cn, "used", "") + // Only emit metric decrements if we actually removed the connection from the map. + // If removed is false, Close() already removed it and emitted the -1 delta. + if removed { + // Notify metrics: connection removed (assume from used state) + if cb := getMetricConnectionStateChangeCallback(); cb != nil { + cb(ctx, cn, MetricStateUsed, "") + } + // Record connection count decrement (connection removed, assume from used state) + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(ctx, -1, cn, "used", false) + } } - // Record connection closed - if cb := getMetricConnectionClosedCallback(); cb != nil { - reasonStr := "unknown" - if reason != nil { - reasonStr = reason.Error() + // Only emit connection closed if we actually owned the removal. + // If removed is false, Close() already emitted connectionClosed for this conn. + if removed { + if cb := getMetricConnectionClosedCallback(); cb != nil { + reasonStr := "unknown" + if reason != nil { + reasonStr = reason.Error() + } + cb(ctx, cn, reasonStr, reason) } - cb(ctx, cn, reasonStr, reason) } _ = p.closeConn(cn) @@ -1211,19 +1438,64 @@ func (p *ConnPool) removeConnInternal(ctx context.Context, cn *Conn, reason erro p.checkMinIdleConns() } -func (p *ConnPool) CloseConn(cn *Conn) error { - p.removeConnWithLock(cn) +// CloseConn closes a connection and records metrics. +// Parameters: +// - ctx: context for metric callbacks (enables trace-to-metric correlation) +// - cn: the connection to close +// - reason: why the connection is being closed (use CloseReason* constants) +// - fromState: the metric state the connection was in (use MetricState* constants) +func (p *ConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error { + if hookManager := p.hookManager.Load(); hookManager != nil { + hookManager.ProcessOnRemove(ctx, cn, errors.New(reason)) + } + + removed := p.removeConnWithLock(cn) + + // Only emit UpDownCounter decrements if we actually removed the connection. + // If removed is false, Close() already removed it and emitted the -1 delta. + // Only emit connection closed if we actually owned the removal. + // If removed is false, Close() already emitted connectionClosed for this conn. + if removed { + p.recordConnectionMetrics(ctx, cn, reason, fromState) + } + return p.closeConn(cn) } -func (p *ConnPool) removeConnWithLock(cn *Conn) { +func (p *ConnPool) recordConnectionMetrics(ctx context.Context, cn *Conn, reason string, fromState string) { + // Record connection state change: connection is being removed from the specified state + if cb := getMetricConnectionStateChangeCallback(); cb != nil && fromState != "" { + cb(ctx, cn, fromState, "") + } + + // Record connection count decrement (UpDownCounter) for the state the connection was in + if cb := getMetricConnectionCountCallback(); cb != nil && fromState != "" { + cb(ctx, -1, cn, fromState, false) + } + + if cb := getMetricConnectionClosedCallback(); cb != nil { + cb(ctx, cn, reason, nil) + } +} + +// removeConnWithLock removes a connection from the pool under the connsMu lock. +// Returns true if the connection was actually present in p.conns and was removed, +// false if it was already gone (e.g., removed by Close()). Callers must use the +// return value to decide whether to emit metric decrements β€” this eliminates the +// shutdown race between Close() and concurrent removal paths. +func (p *ConnPool) removeConnWithLock(cn *Conn) bool { p.connsMu.Lock() defer p.connsMu.Unlock() - p.removeConn(cn) + return p.removeConn(cn) } -func (p *ConnPool) removeConn(cn *Conn) { +// removeConn removes a connection from the pool's internal data structures. +// Returns true if the connection was present and removed, false otherwise. +func (p *ConnPool) removeConn(cn *Conn) bool { cid := cn.GetID() + if _, exists := p.conns[cid]; !exists { + return false + } delete(p.conns, cid) atomic.AddUint32(&p.stats.StaleConns, 1) @@ -1239,6 +1511,7 @@ func (p *ConnPool) removeConn(cn *Conn) { } } } + return true } func (p *ConnPool) closeConn(cn *Conn) error { @@ -1290,13 +1563,33 @@ func (p *ConnPool) closed() bool { } func (p *ConnPool) Filter(fn func(*Conn) bool) error { + ctx := context.Background() + p.connsMu.Lock() defer p.connsMu.Unlock() + idleConnSet := make(map[*Conn]struct{}, len(p.idleConns)) + for _, ic := range p.idleConns { + idleConnSet[ic] = struct{}{} + } + var firstErr error for _, cn := range p.conns { if fn(cn) { - if err := p.closeConn(cn); err != nil && firstErr == nil { + var err error + if _, isIdle := idleConnSet[cn]; isIdle { + // Idle connection - remove from pool and close. + p.removeConn(cn) + p.recordConnectionMetrics(ctx, cn, CloseReasonFailover, MetricStateIdle) + err = p.closeConn(cn) + } else { + // Used connection - set closeReason and close the connection. + // The connection remains in p.conns. When putConn() is called later, + // it will close the connection instead of pooling it. + cn.closeReason.Store(CloseReasonFailover) + err = cn.Close() + } + if err != nil && firstErr == nil { firstErr = err } } @@ -1310,10 +1603,48 @@ func (p *ConnPool) Close() error { } var firstErr error + nowNs := time.Now().UnixNano() p.connsMu.Lock() + + // Emit -1 for each connection. Since all idle↔used transitions happen + // under connsMu, the idleConns slice is the source of truth for state. + cb := getMetricConnectionCountCallback() + idleSet := make(map[uint64]struct{}, len(p.idleConns)) + for _, cn := range p.idleConns { + idleSet[cn.GetID()] = struct{}{} + } + ctx := context.Background() for _, cn := range p.conns { + // Check health before closing, since closeConn invalidates the + // underlying fd and would make connCheck (inside isHealthyConn) + // always fail with EBADF. + // Only check health for idle connections to avoid data races when + // peeking at the socket/reader while another goroutine is reading from it. + // Non-idle connections are either in use or in transitional states and + // shouldn't be health-checked during shutdown. + _, isIdle := idleSet[cn.GetID()] + var healthy bool + if isIdle { + healthy = p.isHealthyConn(cn, nowNs) + } else { + healthy = true + } + if cb != nil { + if isIdle { + cb(ctx, -1, cn, "idle", false) + } else { + cb(ctx, -1, cn, "used", false) + } + } + if closedCb := getMetricConnectionClosedCallback(); closedCb != nil { + closedCb(ctx, cn, "pool_shutdown", nil) + } if err := p.closeConn(cn); err != nil && firstErr == nil { - firstErr = err + // Suppress close errors for stale connections, consistent + // with how Get() handles them (see CloseReasonStale path). + if healthy { + firstErr = err + } } } p.conns = nil diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go index 365219a5..68295906 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_single.go @@ -33,8 +33,8 @@ func (p *SingleConnPool) NewConn(ctx context.Context) (*Conn, error) { return p.pool.NewConn(ctx) } -func (p *SingleConnPool) CloseConn(cn *Conn) error { - return p.pool.CloseConn(cn) +func (p *SingleConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error { + return p.pool.CloseConn(ctx, cn, reason, fromState) } func (p *SingleConnPool) Get(_ context.Context) (*Conn, error) { diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go index be869b56..6763299e 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pool_sticky.go @@ -61,8 +61,8 @@ func (p *StickyConnPool) NewConn(ctx context.Context) (*Conn, error) { return p.pool.NewConn(ctx) } -func (p *StickyConnPool) CloseConn(cn *Conn) error { - return p.pool.CloseConn(cn) +func (p *StickyConnPool) CloseConn(ctx context.Context, cn *Conn, reason string, fromState string) error { + return p.pool.CloseConn(ctx, cn, reason, fromState) } func (p *StickyConnPool) Get(ctx context.Context) (*Conn, error) { diff --git a/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go b/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go index e566d42b..8cfa8678 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go +++ b/vendor/github.com/redis/go-redis/v9/internal/pool/pubsub.go @@ -53,18 +53,42 @@ func (p *PubSubPool) NewConn(ctx context.Context, network string, addr string, c func (p *PubSubPool) TrackConn(cn *Conn) { atomic.AddUint32(&p.stats.Active, 1) p.activeConns.Store(cn.GetID(), cn) + // Emit +1 used for PubSub connection + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(context.Background(), 1, cn, "used", true) + } } func (p *PubSubPool) UntrackConn(cn *Conn) { + // LoadAndDelete ensures each connection is only decremented once, + // guarding against double-decrement if Close() already untracked it. + if _, loaded := p.activeConns.LoadAndDelete(cn.GetID()); !loaded { + return + } atomic.AddUint32(&p.stats.Active, ^uint32(0)) atomic.AddUint32(&p.stats.Untracked, 1) - p.activeConns.Delete(cn.GetID()) + // Emit -1 used for PubSub connection + if cb := getMetricConnectionCountCallback(); cb != nil { + cb(context.Background(), -1, cn, "used", true) + } } func (p *PubSubPool) Close() error { p.closed.Store(true) + cb := getMetricConnectionCountCallback() p.activeConns.Range(func(key, value interface{}) bool { cn := value.(*Conn) + // Use LoadAndDelete to atomically claim ownership of this entry. + // If a concurrent UntrackConn already removed it, skip to avoid double-decrement. + if _, loaded := p.activeConns.LoadAndDelete(key); !loaded { + return true + } + atomic.AddUint32(&p.stats.Active, ^uint32(0)) + atomic.AddUint32(&p.stats.Untracked, 1) + // Emit -1 used for each PubSub connection being closed + if cb != nil { + cb(context.Background(), -1, cn, "used", true) + } _ = cn.Close() return true }) diff --git a/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go b/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go index bac68f79..83f28e4d 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go +++ b/vendor/github.com/redis/go-redis/v9/internal/proto/reader.go @@ -100,9 +100,20 @@ func (r *Reader) PeekReplyType() (byte, error) { return b[0], nil } +// PeekPushNotificationName returns the notification name of the next RESP3 +// push frame without consuming it. The caller is expected to have already +// verified that the next reply is a push notification (e.g. via PeekReplyType +// returning RespPush). +// +// To identify the name the method may block briefly reading more bytes from +// the underlying connection. That is safe: once the push marker '>' has been +// observed, the server is committed to sending the rest of the frame, so +// fetching the next few header bytes does not race with anything the caller +// could be waiting on. Blocking is preferred to a truncated peek, which would +// silently misidentify the notification and cause the caller's ReadReply to +// consume (and drop) the frame; see issue #3839. func (r *Reader) PeekPushNotificationName() (string, error) { - // "prime" the buffer by peeking at the next byte - c, err := r.Peek(1) + c, err := r.rd.Peek(1) if err != nil { return "", err } @@ -110,80 +121,138 @@ func (r *Reader) PeekPushNotificationName() (string, error) { return "", fmt.Errorf("redis: can't peek push notification name, next reply is not a push notification") } - // peek 36 bytes at most, should be enough to read the push notification name - toPeek := 36 - buffered := r.Buffered() - if buffered == 0 { - return "", fmt.Errorf("redis: can't peek push notification name, no data available") - } - if buffered < toPeek { - toPeek = buffered + // Start with a peek window that covers every Redis-defined notification + // header (MOVING, MIGRATING, FAILED_OVER, message, pmessage, smessage, + // subscribe, unsubscribe, ...). If a longer name is encountered, grow + // the window up to maxPushHeaderPeek before giving up. + const initialPeek = 36 + const maxPushHeaderPeek = 4096 + + peekSize := initialPeek + for { + buf, peekErr := r.rd.Peek(peekSize) + name, complete, parseErr := parsePushNotificationName(buf) + if parseErr != nil { + return "", parseErr + } + if complete { + return name, nil + } + // Parser ran out of bytes. Surface a failed underlying read before + // growing further; otherwise grow the peek window and retry. + if peekErr != nil { + return "", peekErr + } + if peekSize >= maxPushHeaderPeek { + return "", fmt.Errorf("redis: push notification header exceeds %d bytes", maxPushHeaderPeek) + } + peekSize *= 2 + if peekSize > maxPushHeaderPeek { + peekSize = maxPushHeaderPeek + } } - buf, err := r.rd.Peek(toPeek) - if err != nil { - return "", err +} + +// parsePushNotificationName extracts the notification name from a buffered +// RESP3 push frame prefix. The three return values are: +// +// - (name, true, nil): the full name is in buf. +// - ("", false, nil): buf is a valid prefix but too short to determine the +// name; the caller should fetch more bytes and retry. +// - ("", _, err): buf is malformed. +// +// This split lets PeekPushNotificationName tell "incomplete header" apart +// from "corrupt frame" without ever returning a truncated string. +func parsePushNotificationName(buf []byte) (string, bool, error) { + // Need at least ">N\r" before any meaningful work. + if len(buf) < 3 { + return "", false, nil } if buf[0] != RespPush { - return "", fmt.Errorf("redis: can't parse push notification: %q", buf) + return "", false, fmt.Errorf("redis: can't parse push notification: %q", buf) } - if len(buf) < 3 { - return "", fmt.Errorf("redis: can't parse push notification: %q", buf) + // Skip the array length line ">N\r\n". + const arrayLenStart = 1 // first byte after the '>' marker + pos, ok, err := skipDigitsThenCRLF(buf, arrayLenStart) + if err != nil { + return "", false, fmt.Errorf("redis: can't parse push notification: %w", err) } - - // remove push notification type - buf = buf[1:] - // remove first line - e.g. >2\r\n - for i := 0; i < len(buf)-1; i++ { - if buf[i] == '\r' && buf[i+1] == '\n' { - buf = buf[i+2:] - break - } else { - if buf[i] < '0' || buf[i] > '9' { - return "", fmt.Errorf("redis: can't parse push notification: %q", buf) - } - } + if !ok { + return "", false, nil + } + // Reject ">\r\n": RESP requires at least one digit for the array length. + // Without this check the empty length looks like a valid prefix and the + // caller would block fetching more bytes for a frame that is already + // malformed. + if pos-2 == arrayLenStart { + return "", false, fmt.Errorf("redis: empty push notification array length") } - if len(buf) < 2 { - return "", fmt.Errorf("redis: can't parse push notification: %q", buf) + + // First element type byte: '$' (bulk) or '+' (simple-string). + if pos >= len(buf) { + return "", false, nil } - // next line should be $\r\n or +\r\n - // should have the type of the push notification name and it's length - if buf[0] != RespString && buf[0] != RespStatus { - return "", fmt.Errorf("redis: can't parse push notification name: %q", buf) + typeOfName := buf[pos] + if typeOfName != RespString && typeOfName != RespStatus { + return "", false, fmt.Errorf("redis: can't parse push notification name: %q", buf[pos:]) } - typeOfName := buf[0] - // remove the type of the push notification name - buf = buf[1:] + pos++ + if typeOfName == RespString { - // remove the length of the string - if len(buf) < 2 { - return "", fmt.Errorf("redis: can't parse push notification name: %q", buf) - } - for i := 0; i < len(buf)-1; i++ { - if buf[i] == '\r' && buf[i+1] == '\n' { - buf = buf[i+2:] - break - } else { - if buf[i] < '0' || buf[i] > '9' { - return "", fmt.Errorf("redis: can't parse push notification name: %q", buf) - } - } + // Read "$M\r\n" then the M-byte name. + lenStart := pos + next, ok, err := skipDigitsThenCRLF(buf, pos) + if err != nil { + return "", false, fmt.Errorf("redis: can't parse push notification name length: %w", err) + } + if !ok { + return "", false, nil + } + if next-2 == lenStart { + return "", false, fmt.Errorf("redis: empty push notification name length") } + nameLen, err := util.Atoi(buf[lenStart : next-2]) + if err != nil { + return "", false, fmt.Errorf("redis: invalid push notification name length %q: %w", buf[lenStart:next-2], err) + } + if nameLen < 0 { + return "", false, fmt.Errorf("redis: negative push notification name length: %d", nameLen) + } + // Compare against the remaining bytes instead of computing + // next+nameLen: a hugely advertised length on malformed input could + // overflow int, wrap negative, slip past an "end > len(buf)" guard and + // panic the slice below. next <= len(buf) here, so the subtraction is + // safe. + if nameLen > len(buf)-next { + return "", false, nil + } + return util.BytesToString(buf[next : next+nameLen]), true, nil } - if len(buf) < 2 { - return "", fmt.Errorf("redis: can't parse push notification name: %q", buf) - } - // keep only the notification name - for i := 0; i < len(buf)-1; i++ { + // RespStatus: scan for the terminating CRLF. + for i := pos; i < len(buf)-1; i++ { if buf[i] == '\r' && buf[i+1] == '\n' { - buf = buf[:i] - break + return util.BytesToString(buf[pos:i]), true, nil } } + return "", false, nil +} - return util.BytesToString(buf), nil +// skipDigitsThenCRLF advances past zero-or-more ASCII digits and the +// terminating "\r\n" starting at offset start in buf. It returns the position +// after the "\r\n" and true on success; (pos, false, nil) if buf is too +// short; or an error if a non-digit non-CR byte is encountered before the CRLF. +func skipDigitsThenCRLF(buf []byte, start int) (int, bool, error) { + for pos := start; pos < len(buf)-1; pos++ { + if buf[pos] == '\r' && buf[pos+1] == '\n' { + return pos + 2, true, nil + } + if buf[pos] < '0' || buf[pos] > '9' { + return pos, false, fmt.Errorf("expected digit or CRLF, got %q", buf[pos]) + } + } + return len(buf), false, nil } // ReadLine Return a valid reply, it will check the protocol or redis error, @@ -279,8 +348,8 @@ func (r *Reader) ReadReply() (interface{}, error) { } func (r *Reader) readFloat(line []byte) (float64, error) { - v := string(line[1:]) - switch string(line[1:]) { + v := util.BytesToString(line[1:]) + switch v { case "inf": return math.Inf(1), nil case "-inf": @@ -292,7 +361,7 @@ func (r *Reader) readFloat(line []byte) (float64, error) { } func (r *Reader) readBool(line []byte) (bool, error) { - switch string(line[1:]) { + switch util.BytesToString(line[1:]) { case "t": return true, nil case "f": @@ -303,7 +372,7 @@ func (r *Reader) readBool(line []byte) (bool, error) { func (r *Reader) readBigInt(line []byte) (*big.Int, error) { i := new(big.Int) - if i, ok := i.SetString(string(line[1:]), 10); ok { + if i, ok := i.SetString(util.BytesToString(line[1:]), 10); ok { return i, nil } return nil, fmt.Errorf("redis: can't parse bigInt reply: %q", line) @@ -453,7 +522,7 @@ func (r *Reader) ReadFloat() (float64, error) { case RespFloat: return r.readFloat(line) case RespStatus: - return strconv.ParseFloat(string(line[1:]), 64) + return strconv.ParseFloat(util.BytesToString(line[1:]), 64) case RespString: s, err := r.readStringReply(line) if err != nil { @@ -646,3 +715,193 @@ func IsNilReply(line []byte) bool { (line[0] == RespString || line[0] == RespArray) && line[1] == '-' && line[2] == '1' } + +// ReadRawReply reads the next RESP reply and returns it as raw bytes without parsing. +func (r *Reader) ReadRawReply() ([]byte, error) { + return r.readRawReplyBuf(nil) +} + +func (r *Reader) readRawReplyBuf(buf []byte) ([]byte, error) { + line, err := r.readLine() + if err != nil { + return buf, err + } + + buf = append(buf, line...) + buf = append(buf, '\r', '\n') + + switch line[0] { + case RespStatus, RespError, RespInt, RespNil, RespFloat, RespBool, RespBigInt: + return buf, nil + + case RespString, RespVerbatim, RespBlobError: + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + curLen := len(buf) + buf = append(buf, make([]byte, n+2)...) + _, err = io.ReadFull(r.rd, buf[curLen:]) + return buf, err + + case RespArray, RespSet, RespPush: + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + for i := 0; i < n; i++ { + buf, err = r.readRawReplyBuf(buf) + if err != nil { + return buf, err + } + } + return buf, nil + + case RespMap: + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + for i := 0; i < n*2; i++ { + buf, err = r.readRawReplyBuf(buf) + if err != nil { + return buf, err + } + } + return buf, nil + + case RespAttr: + // Per RESP3 spec, an attribute is always followed by the actual command reply. + // We need to read the attribute's key-value pairs AND the following reply. + n, err := replyLen(line) + if err != nil { + if err == Nil { + return buf, nil + } + return buf, err + } + // Read the attribute key-value pairs + for i := 0; i < n*2; i++ { + buf, err = r.readRawReplyBuf(buf) + if err != nil { + return buf, err + } + } + // Read the command reply that follows the attribute + return r.readRawReplyBuf(buf) + } + + return buf, fmt.Errorf("redis: can't read raw reply: %.100q", line) +} + +var crlf = []byte{'\r', '\n'} + +// ReadRawReplyWriteTo streams the next RESP reply directly to w without intermediate allocations. +// Returns the number of bytes written and any error encountered. +func (r *Reader) ReadRawReplyWriteTo(w io.Writer) (int64, error) { + return r.readRawReplyWriteTo(w) +} + +func (r *Reader) readRawReplyWriteTo(w io.Writer) (int64, error) { + line, err := r.readLine() + if err != nil { + return 0, err + } + + var written int64 + n, err := w.Write(line) + written += int64(n) + if err != nil { + return written, err + } + n, err = w.Write(crlf) + written += int64(n) + if err != nil { + return written, err + } + + switch line[0] { + case RespStatus, RespError, RespInt, RespNil, RespFloat, RespBool, RespBigInt: + return written, nil + + case RespString, RespVerbatim, RespBlobError: + dataLen, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + copied, err := io.CopyN(w, r.rd, int64(dataLen)+2) + written += copied + return written, err + + case RespArray, RespSet, RespPush: + count, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + for i := 0; i < count; i++ { + n, err := r.readRawReplyWriteTo(w) + written += n + if err != nil { + return written, err + } + } + return written, nil + + case RespMap: + count, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + for i := 0; i < count*2; i++ { + n, err := r.readRawReplyWriteTo(w) + written += n + if err != nil { + return written, err + } + } + return written, nil + + case RespAttr: + // Per RESP3 spec, an attribute is always followed by the actual command reply. + // We need to read the attribute's key-value pairs AND the following reply. + count, err := replyLen(line) + if err != nil { + if err == Nil { + return written, nil + } + return written, err + } + // Read the attribute key-value pairs + for i := 0; i < count*2; i++ { + n, err := r.readRawReplyWriteTo(w) + written += n + if err != nil { + return written, err + } + } + // Read the command reply that follows the attribute + n, err := r.readRawReplyWriteTo(w) + written += n + return written, err + } + + return written, fmt.Errorf("redis: can't read raw reply: %.100q", line) +} diff --git a/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go b/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go index a28240f5..a75370cf 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go +++ b/vendor/github.com/redis/go-redis/v9/internal/proto/redis_errors.go @@ -309,13 +309,25 @@ func IsReadOnlyError(err error) bool { if errors.As(err, &readOnlyErr) { return true } - // Check if wrapped error is a RedisError with READONLY prefix + // Check if wrapped error is a RedisError with READONLY prefix or Lua script READONLY var redisErr RedisError - if errors.As(err, &redisErr) && strings.HasPrefix(redisErr.Error(), "READONLY ") { - return true + if errors.As(err, &redisErr) { + s := redisErr.Error() + if strings.HasPrefix(s, "READONLY ") { + return true + } + // Lua script wrapped READONLY errors: + // "ERR Error running script (call to f_): @user_script:N: -READONLY You can't write against a read only replica." + if strings.Contains(s, "-READONLY You can't write against a read only replica") { + return true + } } // Fallback to string checking for backward compatibility - return strings.HasPrefix(err.Error(), "READONLY ") + s := err.Error() + if strings.HasPrefix(s, "READONLY ") { + return true + } + return strings.Contains(s, "-READONLY You can't write against a read only replica") } // IsMovedError checks if an error is a MovedError, even if wrapped. diff --git a/vendor/github.com/redis/go-redis/v9/internal/rand/rand.go b/vendor/github.com/redis/go-redis/v9/internal/rand/rand.go deleted file mode 100644 index 2edccba9..00000000 --- a/vendor/github.com/redis/go-redis/v9/internal/rand/rand.go +++ /dev/null @@ -1,50 +0,0 @@ -package rand - -import ( - "math/rand" - "sync" -) - -// Int returns a non-negative pseudo-random int. -func Int() int { return pseudo.Int() } - -// Intn returns, as an int, a non-negative pseudo-random number in [0,n). -// It panics if n <= 0. -func Intn(n int) int { return pseudo.Intn(n) } - -// Int63n returns, as an int64, a non-negative pseudo-random number in [0,n). -// It panics if n <= 0. -func Int63n(n int64) int64 { return pseudo.Int63n(n) } - -// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n). -func Perm(n int) []int { return pseudo.Perm(n) } - -// Seed uses the provided seed value to initialize the default Source to a -// deterministic state. If Seed is not called, the generator behaves as if -// seeded by Seed(1). -func Seed(n int64) { pseudo.Seed(n) } - -var pseudo = rand.New(&source{src: rand.NewSource(1)}) - -type source struct { - src rand.Source - mu sync.Mutex -} - -func (s *source) Int63() int64 { - s.mu.Lock() - n := s.src.Int63() - s.mu.Unlock() - return n -} - -func (s *source) Seed(seed int64) { - s.mu.Lock() - s.src.Seed(seed) - s.mu.Unlock() -} - -// Shuffle pseudo-randomizes the order of elements. -// n is the number of elements. -// swap swaps the elements with indexes i and j. -func Shuffle(n int, swap func(i, j int)) { pseudo.Shuffle(n, swap) } diff --git a/vendor/github.com/redis/go-redis/v9/internal/util.go b/vendor/github.com/redis/go-redis/v9/internal/util.go index f77775ff..00516075 100644 --- a/vendor/github.com/redis/go-redis/v9/internal/util.go +++ b/vendor/github.com/redis/go-redis/v9/internal/util.go @@ -2,6 +2,7 @@ package internal import ( "context" + "math" "net" "strconv" "strings" @@ -10,6 +11,29 @@ import ( "github.com/redis/go-redis/v9/internal/util" ) +// String representations of special float values. +// Values are lowercase for consistency with Redis RESP2 protocol responses. +const ( + NaN = "nan" // Not a Number + Inf = "inf" // Positive infinity + NInf = "-inf" // Negative infinity +) + +// FormatFloat formats a float64 to string, normalizing special values +// (NaN, Inf) to lowercase for consistency with Redis RESP2 protocol. +func FormatFloat(f float64) string { + switch { + case math.IsNaN(f): + return NaN + case math.IsInf(f, 1): + return Inf + case math.IsInf(f, -1): + return NInf + default: + return strconv.FormatFloat(f, 'f', -1, 64) + } +} + func Sleep(ctx context.Context, dur time.Duration) error { t := time.NewTimer(dur) defer t.Stop() diff --git a/vendor/github.com/redis/go-redis/v9/json.go b/vendor/github.com/redis/go-redis/v9/json.go index 781cc468..2bcad0b7 100644 --- a/vendor/github.com/redis/go-redis/v9/json.go +++ b/vendor/github.com/redis/go-redis/v9/json.go @@ -35,6 +35,7 @@ type JSONCmdable interface { JSONObjLen(ctx context.Context, key, path string) *IntPointerSliceCmd JSONSet(ctx context.Context, key, path string, value interface{}) *StatusCmd JSONSetMode(ctx context.Context, key, path string, value interface{}, mode string) *StatusCmd + JSONSetWithArgs(ctx context.Context, key, path string, value interface{}, options *JSONSetArgsOptions) *StatusCmd JSONStrAppend(ctx context.Context, key, path, value string) *IntPointerSliceCmd JSONStrLen(ctx context.Context, key, path string) *IntPointerSliceCmd JSONToggle(ctx context.Context, key, path string) *IntPointerSliceCmd @@ -57,6 +58,25 @@ type JSONArrTrimArgs struct { Stop *int } +// FPHAType is the floating-point type used for storing FP homogeneous arrays +// in JSON.SET (Redis 8.8+). +type FPHAType string + +const ( + FPHATypeBF16 FPHAType = "BF16" + FPHATypeFP16 FPHAType = "FP16" + FPHATypeFP32 FPHAType = "FP32" + FPHATypeFP64 FPHAType = "FP64" +) + +// JSONSetArgsOptions are the optional arguments for JSONSetWithArgs. +// Mode is "NX" or "XX" (case-insensitive). FPHA, when set, forces Redis to +// store all FP homogeneous arrays using the specified floating-point type. +type JSONSetArgsOptions struct { + Mode string + FPHA FPHAType +} + type JSONCmd struct { baseCmd val string @@ -584,6 +604,15 @@ func (c cmdable) JSONSet(ctx context.Context, key, path string, value interface{ // the argument is a string or []byte when we assume that it can be passed directly as JSON. // For more information, see https://redis.io/commands/json.set func (c cmdable) JSONSetMode(ctx context.Context, key, path string, value interface{}, mode string) *StatusCmd { + return c.JSONSetWithArgs(ctx, key, path, value, &JSONSetArgsOptions{Mode: mode}) +} + +// JSONSetWithArgs sets the JSON value at the given path in the given key with optional arguments +// for setting mode (NX/XX) and the FPHA (Floating-Point Homogeneous Array) type used for storing +// FP arrays. The value must be something that can be marshaled to JSON (using encoding/JSON) unless +// the argument is a string or []byte when we assume that it can be passed directly as JSON. +// For more information, see https://redis.io/commands/json.set +func (c cmdable) JSONSetWithArgs(ctx context.Context, key, path string, value interface{}, options *JSONSetArgsOptions) *StatusCmd { var bytes []byte var err error switch v := value.(type) { @@ -595,13 +624,17 @@ func (c cmdable) JSONSetMode(ctx context.Context, key, path string, value interf bytes, err = json.Marshal(v) } args := []interface{}{"JSON.SET", key, path, util.BytesToString(bytes)} - if mode != "" { - switch strings.ToUpper(mode) { - case "XX", "NX": - args = append(args, strings.ToUpper(mode)) - - default: - panic("redis: JSON.SET mode must be NX or XX") + if options != nil { + if options.Mode != "" { + switch strings.ToUpper(options.Mode) { + case "XX", "NX": + args = append(args, strings.ToUpper(options.Mode)) + default: + panic("redis: JSON.SET mode must be NX or XX") + } + } + if options.FPHA != "" { + args = append(args, "FPHA", string(options.FPHA)) } } cmd := NewStatusCmd(ctx, args...) diff --git a/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go b/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go index db666f3a..70d5acdc 100644 --- a/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go +++ b/vendor/github.com/redis/go-redis/v9/maintnotifications/config.go @@ -4,7 +4,6 @@ import ( "context" "net" "runtime" - "strings" "time" "github.com/redis/go-redis/v9/internal" @@ -364,20 +363,46 @@ func (c *Config) applyWorkerDefaults(poolSize int) { } } +// endpointDetectResolveTimeout bounds the DNS lookup performed by +// DetectEndpointType so a slow or broken resolver cannot block client +// construction for the full system resolver timeout (often 5-30s). +const endpointDetectResolveTimeout = 2 * time.Second + +// cgnatNet is RFC6598 shared address space (100.64.0.0/10), used by many +// cloud/carrier NATs and not covered by net.IP.IsPrivate. +var cgnatNet = &net.IPNet{IP: net.IPv4(100, 64, 0, 0), Mask: net.CIDRMask(10, 32)} + +// isPrivateIP reports whether ip belongs to a range that should be treated +// as "internal" for the purpose of endpoint type detection. It extends +// net.IP.IsPrivate (RFC1918 + RFC4193) with loopback, link-local and +// RFC6598 shared address space (CGNAT). +func isPrivateIP(ip net.IP) bool { + if ip == nil { + return false + } + if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() { + return true + } + if v4 := ip.To4(); v4 != nil && cgnatNet.Contains(v4) { + return true + } + return false +} + // DetectEndpointType automatically detects the appropriate endpoint type // based on the connection address and TLS configuration. // -// For IP addresses: +// TLS behaviour: // - If TLS is enabled: requests FQDN for proper certificate validation -// - If TLS is disabled: requests IP for better performance -// -// For hostnames: -// - If TLS is enabled: always requests FQDN for proper certificate validation -// - If TLS is disabled: requests IP for better performance +// (SNI / hostname verification). +// - If TLS is disabled: always requests IP for better performance, even +// when the configured address is a hostname. In that case the hostname +// is resolved to determine whether it belongs to an internal or +// external network range. // // Internal vs External detection: // - For IPs: uses private IP range detection -// - For hostnames: uses heuristics based on common internal naming patterns +// - For hostnames: resolves the hostname to an IP address and uses the IP range detection func DetectEndpointType(addr string, tlsEnabled bool) EndpointType { // Extract host from "host:port" format host, _, err := net.SplitHostPort(addr) @@ -385,6 +410,16 @@ func DetectEndpointType(addr string, tlsEnabled bool) EndpointType { host = addr // Assume no port } + // An empty host (e.g., ":6379") conventionally means the loopback + // interface and is treated as internal. With TLS off we return an IP + // endpoint; with TLS on the caller still needs an FQDN for SNI. + if host == "" { + if tlsEnabled { + return EndpointTypeInternalFQDN + } + return EndpointTypeInternalIP + } + // Check if the host is an IP address or hostname ip := net.ParseIP(host) isIPAddress := ip != nil @@ -392,7 +427,7 @@ func DetectEndpointType(addr string, tlsEnabled bool) EndpointType { if isIPAddress { // Address is an IP - determine if it's private or public - isPrivate := ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() + isPrivate := isPrivateIP(ip) if tlsEnabled { // TLS with IP addresses - still prefer FQDN for certificate validation @@ -410,48 +445,58 @@ func DetectEndpointType(addr string, tlsEnabled bool) EndpointType { } } } else { - // Address is a hostname - isInternalHostname := isInternalHostname(host) - if isInternalHostname { - endpointType = EndpointTypeInternalFQDN + // Address is a hostname - resolve it under a bounded timeout so a + // slow/broken DNS server cannot stall client construction. + ctx, cancel := context.WithTimeout(context.Background(), endpointDetectResolveTimeout) + defer cancel() + + isInternal, err := isInternalHostname(ctx, host) + // Will fallback to external classification if we can't determine + // whether the hostname is internal. + if err != nil && internal.LogLevel.WarnOrAbove() { + internal.Logger.Printf(ctx, "Failed to determine if hostname %q is internal: %v", host, err) + } + + if tlsEnabled { + // With TLS the server name must be preserved for certificate + // validation, so request an FQDN endpoint. + if isInternal { + endpointType = EndpointTypeInternalFQDN + } else { + endpointType = EndpointTypeExternalFQDN + } } else { - endpointType = EndpointTypeExternalFQDN + // Without TLS we always prefer IP endpoints for performance, + // even if the configured address is a hostname. + if isInternal { + endpointType = EndpointTypeInternalIP + } else { + endpointType = EndpointTypeExternalIP + } } } return endpointType } -// isInternalHostname determines if a hostname appears to be internal/private. -// This is a heuristic based on common naming patterns. -func isInternalHostname(hostname string) bool { - // Convert to lowercase for comparison - hostname = strings.ToLower(hostname) - - // Common internal hostname patterns - internalPatterns := []string{ - "localhost", - ".local", - ".internal", - ".corp", - ".lan", - ".intranet", - ".private", - } - - // Check for exact match or suffix match - for _, pattern := range internalPatterns { - if hostname == pattern || strings.HasSuffix(hostname, pattern) { - return true - } +// isInternalHostname resolves the hostname (both IPv4 and IPv6) under the +// given context and reports whether every resolved address is in a +// private/internal range. If any address is public the hostname is treated +// as external. A resolution error returns (false, err). An empty result set +// returns (false, nil); callers are expected to fall back to an external +// classification when the hostname cannot be determined to be internal. +func isInternalHostname(ctx context.Context, hostname string) (bool, error) { + ips, err := net.DefaultResolver.LookupIPAddr(ctx, hostname) + if err != nil { + return false, err } - - // Check for RFC 1918 style hostnames (e.g., redis-1, db-server, etc.) - // If hostname doesn't contain dots, it's likely internal - if !strings.Contains(hostname, ".") { - return true + if len(ips) == 0 { + return false, nil } - - // Default to external for fully qualified domain names - return false + for _, ia := range ips { + if !isPrivateIP(ia.IP) { + return false, nil + } + } + return true, nil } diff --git a/vendor/github.com/redis/go-redis/v9/options.go b/vendor/github.com/redis/go-redis/v9/options.go index 5db27102..ba45a0cb 100644 --- a/vendor/github.com/redis/go-redis/v9/options.go +++ b/vendor/github.com/redis/go-redis/v9/options.go @@ -5,10 +5,11 @@ import ( "crypto/tls" "errors" "fmt" + "maps" "net" "net/url" "runtime" - "sort" + "slices" "strconv" "strings" "sync/atomic" @@ -143,6 +144,13 @@ type Options struct { // default: 100 milliseconds DialerRetryTimeout time.Duration + // DialerRetryBackoff controls the delay between dial retry attempts. + // + // attempt is 0-based: attempt=0 is the delay after the 1st failed dial (before the 2nd attempt). + // + // If nil, dial retry backoff is constant and equals DialerRetryTimeout (default: 100ms). + DialerRetryBackoff func(attempt int) time.Duration + // ReadTimeout for socket reads. If reached, commands will fail // with a timeout instead of blocking. Supported values: // @@ -282,8 +290,9 @@ type Options struct { // IdentitySuffix - add suffix to client name. IdentitySuffix string - // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. - // When unstable mode is enabled, the client will use RESP3 protocol and only be able to use RawResult + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. UnstableResp3 bool // Push notifications are always enabled for RESP3 connections (Protocol: 3) @@ -644,11 +653,8 @@ func (o *queryOptions) remaining() []string { if len(o.q) == 0 { return nil } - keys := make([]string, 0, len(o.q)) - for k := range o.q { - keys = append(keys, k) - } - sort.Strings(keys) + keys := slices.Collect(maps.Keys(o.q)) + slices.Sort(keys) return keys } @@ -755,6 +761,7 @@ func newConnPool( DialTimeout: opt.DialTimeout, DialerRetries: opt.DialerRetries, DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, MinIdleConns: minIdleConns, MaxIdleConns: maxIdleConns, MaxActiveConns: maxActiveConns, @@ -801,6 +808,7 @@ func newPubSubPool( DialTimeout: opt.DialTimeout, DialerRetries: opt.DialerRetries, DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, MinIdleConns: minIdleConns, MaxIdleConns: maxIdleConns, MaxActiveConns: maxActiveConns, diff --git a/vendor/github.com/redis/go-redis/v9/osscluster.go b/vendor/github.com/redis/go-redis/v9/osscluster.go index 6fb51dc2..efd52960 100644 --- a/vendor/github.com/redis/go-redis/v9/osscluster.go +++ b/vendor/github.com/redis/go-redis/v9/osscluster.go @@ -1,14 +1,17 @@ package redis import ( + "cmp" "context" "crypto/tls" "errors" "fmt" "math" + "math/rand" "net" "net/url" "runtime" + "slices" "sort" "strings" "sync" @@ -21,7 +24,6 @@ import ( "github.com/redis/go-redis/v9/internal/otel" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/proto" - "github.com/redis/go-redis/v9/internal/rand" "github.com/redis/go-redis/v9/internal/routing" "github.com/redis/go-redis/v9/maintnotifications" "github.com/redis/go-redis/v9/push" @@ -104,6 +106,10 @@ type ClusterOptions struct { // default: 100 milliseconds DialerRetryTimeout time.Duration + // DialerRetryBackoff controls the delay between dial retry attempts. + // See Options.DialerRetryBackoff for details. + DialerRetryBackoff func(attempt int) time.Duration + ReadTimeout time.Duration WriteTimeout time.Duration ContextTimeoutEnabled bool @@ -157,7 +163,9 @@ type ClusterOptions struct { IdentitySuffix string // Add suffix to client name. Default is empty. - // UnstableResp3 enables Unstable mode for Redis Search module with RESP3. + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. UnstableResp3 bool // PushNotificationProcessor is the processor for handling push notifications. @@ -429,6 +437,7 @@ func (opt *ClusterOptions) clientOptions() *Options { DialTimeout: opt.DialTimeout, DialerRetries: opt.DialerRetries, DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, @@ -523,7 +532,7 @@ func (n *clusterNode) updateLatency() { if successes == 0 { // If none of the pings worked, set latency to some arbitrarily high value so this node gets // least priority. - latency = float64((maximumNodeLatency) / time.Microsecond) + latency = float64(maximumNodeLatency / time.Microsecond) } else { latency = float64(dur) / float64(successes) } @@ -786,20 +795,6 @@ type clusterSlot struct { nodes []*clusterNode } -type clusterSlotSlice []*clusterSlot - -func (p clusterSlotSlice) Len() int { - return len(p) -} - -func (p clusterSlotSlice) Less(i, j int) bool { - return p[i].start < p[j].start -} - -func (p clusterSlotSlice) Swap(i, j int) { - p[i], p[j] = p[j], p[i] -} - type clusterState struct { nodes *clusterNodes Masters []*clusterNode @@ -823,7 +818,7 @@ func newClusterState( createdAt: time.Now(), } - originHost, _, _ := net.SplitHostPort(origin) + originHost, originPort, _ := net.SplitHostPort(origin) isLoopbackOrigin := isLoopback(originHost) for _, slot := range slots { @@ -835,6 +830,11 @@ func newClusterState( if !isLoopbackOrigin { addr = replaceLoopbackHost(addr, originHost) } + // TLS-only clusters (`--port 0 --tls-port 6379`) report port 0 + // in CLUSTER SLOTS. Fall back to the origin port β€” by definition + // reachable, since it is the port that returned this slot map. + // See https://github.com/redis/go-redis/issues/3726. + addr = replaceZeroPort(addr, originPort) node, err := c.nodes.GetOrCreateWithNodeAddress(addr, nodeAddress) if err != nil { @@ -858,7 +858,9 @@ func newClusterState( }) } - sort.Sort(clusterSlotSlice(c.slots)) + slices.SortFunc(c.slots, func(a, b *clusterSlot) int { + return cmp.Compare(a.start, b.start) + }) time.AfterFunc(time.Minute, func() { nodes.GC(c.generation) @@ -886,6 +888,21 @@ func replaceLoopbackHost(nodeAddr, originHost string) string { return net.JoinHostPort(originHost, nodePort) } +// replaceZeroPort substitutes originPort for a node port of "0", which is +// what CLUSTER SLOTS reports for TLS-only clusters started with +// `--port 0 --tls-port `. Non-zero ports and addresses without a +// recoverable origin port are returned unchanged. +func replaceZeroPort(nodeAddr, originPort string) string { + if originPort == "" || originPort == "0" { + return nodeAddr + } + nodeHost, nodePort, err := net.SplitHostPort(nodeAddr) + if err != nil || nodePort != "0" { + return nodeAddr + } + return net.JoinHostPort(nodeHost, originPort) +} + // isLoopback returns true if the host is a loopback address. // For IP addresses, it uses net.IP.IsLoopback(). // For hostnames, it recognizes well-known loopback hostnames like "localhost" @@ -949,7 +966,7 @@ func (c *clusterState) slotClosestNode(slot int) (*clusterNode, error) { return c.nodes.Random() } - var allNodesFailing = true + allNodesFailing := true var ( closestNonFailingNode *clusterNode closestNode *clusterNode @@ -1139,8 +1156,12 @@ type ClusterClient struct { } // NewClusterClient returns a Redis Cluster client as described in -// http://redis.io/topics/cluster-spec. +// https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec. +// Passing nil ClusterOptions will cause a panic. func NewClusterClient(opt *ClusterOptions) *ClusterClient { + if opt == nil { + panic("redis: NewClusterClient nil options") + } opt.init() c := &ClusterClient{ @@ -1185,7 +1206,8 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient { return c } -// Options returns read-only Options that were used to create the client. +// Options returns read-only *ClusterOptions that were used to create the client. +// Any alteration of the returned *ClusterOptions may result in undefined behaviour. func (c *ClusterClient) Options() *ClusterOptions { return c.opt } @@ -1295,7 +1317,7 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error { continue } - if shouldRetry(lastErr, cmd.readTimeout() == nil) { + if shouldRetry(lastErr, cmd.readTimeout() == nil) && !cmd.NoRetry() { // First retry the same node. if attempt == 0 { continue @@ -1450,6 +1472,8 @@ func (c *ClusterClient) PoolStats() *PoolStats { acc.Hits += s.Hits acc.Misses += s.Misses acc.Timeouts += s.Timeouts + acc.WaitCount += s.WaitCount + acc.WaitDurationNs += s.WaitDurationNs acc.TotalConns += s.TotalConns acc.IdleConns += s.IdleConns @@ -1461,6 +1485,8 @@ func (c *ClusterClient) PoolStats() *PoolStats { acc.Hits += s.Hits acc.Misses += s.Misses acc.Timeouts += s.Timeouts + acc.WaitCount += s.WaitCount + acc.WaitDurationNs += s.WaitDurationNs acc.TotalConns += s.TotalConns acc.IdleConns += s.IdleConns @@ -1711,7 +1737,7 @@ func (c *ClusterClient) processPipelineNodeConn( if isBadConn(err, false, node.Client.getAddr()) { node.MarkAsFailing() } - if shouldRetry(err, true) { + if shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) } setCmdsErr(cmds, err) @@ -1747,7 +1773,7 @@ func (c *ClusterClient) pipelineReadCmds( } if !isRedisError(err) { - if shouldRetry(err, true) { + if shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) } setCmdsErr(cmds[i+1:], err) @@ -1755,7 +1781,7 @@ func (c *ClusterClient) pipelineReadCmds( } } - if err := cmds[0].Err(); err != nil && shouldRetry(err, true) { + if err := cmds[0].Err(); err != nil && shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) return err } @@ -1913,13 +1939,23 @@ func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) err func (c *ClusterClient) slottedKeyedCommands(ctx context.Context, cmds []Cmder) map[int][]Cmder { cmdsSlots := map[int][]Cmder{} + // Peek once outside the loop, one RLock for the whole batch instead of + // two per command (one for the keyless check, one inside cmdSlot). + cachedInfo := c.cmdsInfoCache.Peek() + prefferedRandomSlot := -1 for _, cmd := range cmds { - if cmdFirstKeyPos(cmd) == 0 { + var info *CommandInfo + if cachedInfo != nil { + info = cachedInfo[cmd.Name()] + } + + pos := cmdFirstKeyPosWithInfo(cmd, info) + if pos == 0 { continue } - slot := c.cmdSlot(cmd, prefferedRandomSlot) + slot := c.cmdSlotWithPos(cmd, pos, prefferedRandomSlot) if prefferedRandomSlot == -1 { prefferedRandomSlot = slot } @@ -1958,7 +1994,7 @@ func (c *ClusterClient) processTxPipelineNodeConn( if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error { return writeCmds(wr, cmds) }); err != nil { - if shouldRetry(err, true) { + if shouldRetry(err, true) && !cmdsContainNoRetry(cmds) { _ = c.mapCmdsByNode(ctx, failedCmds, cmds) } setCmdsErr(cmds, err) @@ -2102,10 +2138,18 @@ func (c *ClusterClient) Watch(ctx context.Context, fn func(*Tx) error, keys ...s } } - err = node.Client.Watch(ctx, fn, keys...) + // Track callback errors separately to avoid retrying user failures through cluster retry classification. + var fnErr error + err = node.Client.Watch(ctx, func(tx *Tx) error { + fnErr = fn(tx) + return fnErr + }, keys...) if err == nil { break } + if fnErr != nil { + return fnErr + } moved, ask, addr := isMovedError(err) if moved || ask { @@ -2305,13 +2349,29 @@ func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo { return info } +// cmdInfoPeek returns the cached CommandInfo for the named command without +// triggering a round-trip to Redis. It returns nil when the cache is cold. +func (c *ClusterClient) cmdInfoPeek(name string) *CommandInfo { + if cmds := c.cmdsInfoCache.Peek(); cmds != nil { + return cmds[name] + } + return nil +} + func (c *ClusterClient) cmdSlot(cmd Cmder, prefferedSlot int) int { + info := c.cmdInfoPeek(cmd.Name()) + return c.cmdSlotWithPos(cmd, cmdFirstKeyPosWithInfo(cmd, info), prefferedSlot) +} + +// cmdSlotWithPos computes the cluster slot for cmd given a pre-resolved first key +// position. Separating pos resolution from slot computation lets callers that +// already know pos avoid a redundant Peek() call. +func (c *ClusterClient) cmdSlotWithPos(cmd Cmder, pos int, prefferedSlot int) int { args := cmd.Args() if args[0] == "cluster" && (args[1] == "getkeysinslot" || args[1] == "countkeysinslot") { return args[2].(int) } - - return cmdSlot(cmd, cmdFirstKeyPos(cmd), prefferedSlot) + return cmdSlot(cmd, pos, prefferedSlot) } func cmdSlot(cmd Cmder, pos int, prefferedRandomSlot int) int { diff --git a/vendor/github.com/redis/go-redis/v9/osscluster_router.go b/vendor/github.com/redis/go-redis/v9/osscluster_router.go index 3b001fef..0da29530 100644 --- a/vendor/github.com/redis/go-redis/v9/osscluster_router.go +++ b/vendor/github.com/redis/go-redis/v9/osscluster_router.go @@ -110,7 +110,7 @@ func (c *ClusterClient) executeOnAllShards(ctx context.Context, cmd Cmder, polic // executeMultiShard handles commands that operate on multiple keys across shards func (c *ClusterClient) executeMultiShard(ctx context.Context, cmd Cmder, policy *routing.CommandPolicy) error { args := cmd.Args() - firstKeyPos := int(cmdFirstKeyPos(cmd)) + firstKeyPos := cmdFirstKeyPosWithInfo(cmd, c.cmdInfoPeek(cmd.Name())) stepCount := int(cmd.stepCount()) if stepCount == 0 { stepCount = 1 // Default to 1 if not set @@ -141,11 +141,11 @@ func (c *ClusterClient) executeMultiShard(ctx context.Context, cmd Cmder, policy keyOrder = append(keyOrder, key) } - return c.executeMultiSlot(ctx, cmd, slotMap, keyOrder, policy) + return c.executeMultiSlot(ctx, cmd, slotMap, keyOrder, policy, firstKeyPos) } // executeMultiSlot executes commands across multiple slots concurrently -func (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap map[int][]string, keyOrder []string, policy *routing.CommandPolicy) error { +func (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap map[int][]string, keyOrder []string, policy *routing.CommandPolicy, firstKeyPos int) error { results := make(chan slotResult, len(slotMap)) var wg sync.WaitGroup @@ -162,7 +162,7 @@ func (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap } // Create a command for this specific slot's keys - subCmd := c.createSlotSpecificCommand(ctx, cmd, keys) + subCmd := c.createSlotSpecificCommand(ctx, cmd, keys, firstKeyPos) err = node.Client.Process(ctx, subCmd) results <- slotResult{subCmd, keys, err} }(slot, keys) @@ -176,10 +176,12 @@ func (c *ClusterClient) executeMultiSlot(ctx context.Context, cmd Cmder, slotMap return c.aggregateMultiSlotResults(ctx, cmd, results, keyOrder, policy) } -// createSlotSpecificCommand creates a new command for a specific slot's keys -func (c *ClusterClient) createSlotSpecificCommand(ctx context.Context, originalCmd Cmder, keys []string) Cmder { +// createSlotSpecificCommand creates a new command for a specific slot's keys. +// firstKeyPos is passed in from the caller (computed once in executeMultiShard) +// so this function never independently re-peeks the cache β€” avoids the +// cold --> warm inconsistency the reviewer flagged. +func (c *ClusterClient) createSlotSpecificCommand(ctx context.Context, originalCmd Cmder, keys []string, firstKeyPos int) Cmder { originalArgs := originalCmd.Args() - firstKeyPos := int(cmdFirstKeyPos(originalCmd)) // Build new args with only the specified keys newArgs := make([]interface{}, 0, firstKeyPos+len(keys)) @@ -241,6 +243,8 @@ func createCommandByType(ctx context.Context, cmdType CmdType, args ...interface return NewXPendingExtCmd(ctx, args...) case CmdTypeXAutoClaim: return NewXAutoClaimCmd(ctx, args...) + case CmdTypeXAutoClaimWithDeleted: + return NewXAutoClaimWithDeletedCmd(ctx, args...) case CmdTypeXAutoClaimJustID: return NewXAutoClaimJustIDCmd(ctx, args...) case CmdTypeXInfoStreamFull: @@ -465,7 +469,7 @@ func (c *ClusterClient) createAggregator(policy *routing.CommandPolicy, cmd Cmde } if !isKeyed { - firstKeyPos := cmdFirstKeyPos(cmd) + firstKeyPos := cmdFirstKeyPosWithInfo(cmd, c.cmdInfoPeek(cmd.Name())) isKeyed = firstKeyPos > 0 } @@ -498,7 +502,7 @@ func (c *ClusterClient) pickArbitraryNode(ctx context.Context) *clusterNode { // hasKeys checks if a command operates on keys func (c *ClusterClient) hasKeys(cmd Cmder) bool { - firstKeyPos := cmdFirstKeyPos(cmd) + firstKeyPos := cmdFirstKeyPosWithInfo(cmd, c.cmdInfoPeek(cmd.Name())) return firstKeyPos > 0 } @@ -668,6 +672,12 @@ func (c *ClusterClient) setCommandValue(cmd Cmder, value interface{}) error { c.SetVal(v.messages, v.start) } } + case CmdTypeXAutoClaimWithDeleted: + if c, ok := cmd.(*XAutoClaimWithDeletedCmd); ok { + if v, ok := value.(CmdTypeXAutoClaimWithDeletedValue); ok { + c.SetVal(v.messages, v.start, v.deletedIDs) + } + } case CmdTypeXAutoClaimJustID: if c, ok := cmd.(*XAutoClaimJustIDCmd); ok { if v, ok := value.(CmdTypeXAutoClaimJustIDValue); ok { diff --git a/vendor/github.com/redis/go-redis/v9/otel.go b/vendor/github.com/redis/go-redis/v9/otel.go index a81377d4..1ea35936 100644 --- a/vendor/github.com/redis/go-redis/v9/otel.go +++ b/vendor/github.com/redis/go-redis/v9/otel.go @@ -79,6 +79,25 @@ type OTelRecorder interface { RecordStreamLag(ctx context.Context, lag time.Duration, cn ConnInfo, streamName, consumerGroup, consumerName string) } +// OTelConnectionCounter is an optional capability interface for recording +// connection count and pending request changes via UpDownCounters. +// Implementations of OTelRecorder can optionally implement this interface +// to receive connection count and pending request delta notifications. +// This is kept separate from OTelRecorder to avoid breaking existing +// third-party implementations when new methods are added. +type OTelConnectionCounter interface { + // RecordConnectionCount records a change in connection count (UpDownCounter) + // delta: +1 when connection added, -1 when connection removed + // state: connection state (e.g., "idle", "used") + // isPubSub: true if this is a PubSub connection + RecordConnectionCount(ctx context.Context, delta int, cn ConnInfo, state string, isPubSub bool) + + // RecordPendingRequests records a change in pending requests (UpDownCounter) + // delta: +1 when request starts waiting, -1 when request stops waiting + // poolName is passed explicitly because we may not have a connection yet when request starts + RecordPendingRequests(ctx context.Context, delta int, cn ConnInfo, poolName string) +} + // This is used for async gauge metrics that need to pull stats from pools periodically. type OTelPoolRegistrar interface { // RegisterPool is called when a new client is created with its main connection pool. @@ -163,6 +182,18 @@ func (a *otelRecorderAdapter) RecordStreamLag(ctx context.Context, lag time.Dura a.recorder.RecordStreamLag(ctx, lag, toConnInfo(cn), streamName, consumerGroup, consumerName) } +func (a *otelRecorderAdapter) RecordConnectionCount(ctx context.Context, delta int, cn *pool.Conn, state string, isPubSub bool) { + if counter, ok := a.recorder.(OTelConnectionCounter); ok { + counter.RecordConnectionCount(ctx, delta, toConnInfo(cn), state, isPubSub) + } +} + +func (a *otelRecorderAdapter) RecordPendingRequests(ctx context.Context, delta int, cn *pool.Conn, poolName string) { + if counter, ok := a.recorder.(OTelConnectionCounter); ok { + counter.RecordPendingRequests(ctx, delta, toConnInfo(cn), poolName) + } +} + func (a *otelRecorderAdapter) RegisterPool(poolName string, p pool.Pooler) { if registrar, ok := a.recorder.(OTelPoolRegistrar); ok { registrar.RegisterPool(poolName, &poolerAdapter{p}) diff --git a/vendor/github.com/redis/go-redis/v9/pipeline.go b/vendor/github.com/redis/go-redis/v9/pipeline.go index 567bf121..41b83221 100644 --- a/vendor/github.com/redis/go-redis/v9/pipeline.go +++ b/vendor/github.com/redis/go-redis/v9/pipeline.go @@ -49,7 +49,7 @@ type Pipeliner interface { var _ Pipeliner = (*Pipeline)(nil) // Pipeline implements pipelining as described in -// http://redis.io/topics/pipelining. +// https://redis.io/docs/latest/develop/using-commands/pipelining. // Please note: it is not safe for concurrent use by multiple goroutines. type Pipeline struct { cmdable diff --git a/vendor/github.com/redis/go-redis/v9/pubsub.go b/vendor/github.com/redis/go-redis/v9/pubsub.go index 49eec935..9d696105 100644 --- a/vendor/github.com/redis/go-redis/v9/pubsub.go +++ b/vendor/github.com/redis/go-redis/v9/pubsub.go @@ -3,6 +3,8 @@ package redis import ( "context" "fmt" + "maps" + "slices" "strings" "sync" "time" @@ -15,7 +17,7 @@ import ( ) // PubSub implements Pub/Sub commands as described in -// http://redis.io/topics/pubsub. Message receiving is NOT safe +// https://redis.io/docs/latest/develop/pubsub. Message receiving is NOT safe // for concurrent use by multiple goroutines. // // PubSub automatically reconnects to Redis Server and resubscribes @@ -56,9 +58,9 @@ func (c *PubSub) String() string { c.mu.Lock() defer c.mu.Unlock() - channels := mapKeys(c.channels) - channels = append(channels, mapKeys(c.patterns)...) - channels = append(channels, mapKeys(c.schannels)...) + channels := slices.Collect(maps.Keys(c.channels)) + channels = append(channels, slices.Collect(maps.Keys(c.patterns))...) + channels = append(channels, slices.Collect(maps.Keys(c.schannels))...) return fmt.Sprintf("PubSub(%s)", strings.Join(channels, ", ")) } @@ -85,7 +87,16 @@ func (c *PubSub) conn(ctx context.Context, newChannels []string) (*pool.Conn, er c.opt.Addr = internal.RedisNull } - channels := mapKeys(c.channels) + // Include c.schannels so reconnect-time routing of an SSubscribe-only + // PubSub picks the slot owner (channels[0] in ClusterClient.pubSub()'s + // newConn closure) instead of a random node. + // See https://github.com/redis/go-redis/issues/3806. + // c.patterns is intentionally NOT included: patterns are not slot- + // addressable, and adding them would force PSubscribe-only PubSubs to + // pin to a single node based on pattern-string hash, regressing the + // existing random-node behaviour. + channels := slices.Collect(maps.Keys(c.channels)) + channels = append(channels, slices.Collect(maps.Keys(c.schannels))...) channels = append(channels, newChannels...) cn, err := c.newConn(ctx, c.opt.Addr, channels) @@ -112,18 +123,18 @@ func (c *PubSub) resubscribe(ctx context.Context, cn *pool.Conn) error { var firstErr error if len(c.channels) > 0 { - firstErr = c._subscribe(ctx, cn, "subscribe", mapKeys(c.channels)) + firstErr = c._subscribe(ctx, cn, "subscribe", slices.Collect(maps.Keys(c.channels))) } if len(c.patterns) > 0 { - err := c._subscribe(ctx, cn, "psubscribe", mapKeys(c.patterns)) + err := c._subscribe(ctx, cn, "psubscribe", slices.Collect(maps.Keys(c.patterns))) if err != nil && firstErr == nil { firstErr = err } } if len(c.schannels) > 0 { - err := c._subscribe(ctx, cn, "ssubscribe", mapKeys(c.schannels)) + err := c._subscribe(ctx, cn, "ssubscribe", slices.Collect(maps.Keys(c.schannels))) if err != nil && firstErr == nil { firstErr = err } @@ -132,16 +143,6 @@ func (c *PubSub) resubscribe(ctx context.Context, cn *pool.Conn) error { return firstErr } -func mapKeys(m map[string]struct{}) []string { - s := make([]string, len(m)) - i := 0 - for k := range m { - s[i] = k - i++ - } - return s -} - func (c *PubSub) _subscribe( ctx context.Context, cn *pool.Conn, redisCmd string, channels []string, ) error { @@ -172,6 +173,7 @@ func (c *PubSub) releaseConn(ctx context.Context, cn *pool.Conn, err error, allo if !cn.IsUsable() || cn.ShouldHandoff() { c.reconnect(ctx, fmt.Errorf("pubsub: connection is not usable")) + return } if isBadConn(err, allowTimeout, c.opt.Addr) { @@ -284,9 +286,7 @@ func (c *PubSub) Unsubscribe(ctx context.Context, channels ...string) error { } } else { // Unsubscribe from all channels. - for channel := range c.channels { - delete(c.channels, channel) - } + clear(c.channels) } err := c.subscribe(ctx, "unsubscribe", channels...) @@ -305,9 +305,7 @@ func (c *PubSub) PUnsubscribe(ctx context.Context, patterns ...string) error { } } else { // Unsubscribe from all patterns. - for pattern := range c.patterns { - delete(c.patterns, pattern) - } + clear(c.patterns) } err := c.subscribe(ctx, "punsubscribe", patterns...) @@ -326,9 +324,7 @@ func (c *PubSub) SUnsubscribe(ctx context.Context, channels ...string) error { } } else { // Unsubscribe from all channels. - for channel := range c.schannels { - delete(c.schannels, channel) - } + clear(c.schannels) } err := c.subscribe(ctx, "sunsubscribe", channels...) @@ -366,6 +362,25 @@ func (c *PubSub) Ping(ctx context.Context, payload ...string) error { return err } +// ClientSetName assigns a namee to the PubSub connection using CLIENT SETNAME, +// The name is visible in CLIENT LIST output and is useful for debugging +// and identifying connections in a redis instance. +func (c *PubSub) ClientSetName(ctx context.Context, name string) error { + cmd := NewStatusCmd(ctx, "client", "setname", name) + + c.mu.Lock() + defer c.mu.Unlock() + + cn, err := c.conn(ctx, nil) + if err != nil { + return err + } + + err = c.writeCmd(ctx, cn, cmd) + c.releaseConn(ctx, cn, err, false) + return err +} + // Subscription received after a successful subscription to channel. type Subscription struct { // Can be "subscribe", "unsubscribe", "psubscribe" or "punsubscribe". diff --git a/vendor/github.com/redis/go-redis/v9/redis.go b/vendor/github.com/redis/go-redis/v9/redis.go index 85622e43..dd345189 100644 --- a/vendor/github.com/redis/go-redis/v9/redis.go +++ b/vendor/github.com/redis/go-redis/v9/redis.go @@ -26,6 +26,14 @@ type Scanner = hscan.Scanner // Nil reply returned by Redis when key does not exist. const Nil = proto.Nil +// String representations of special float values. +// Values are lowercase for consistency with Redis RESP2 protocol responses. +const ( + NaN = internal.NaN // Not a Number + Inf = internal.Inf // Positive infinity + NInf = internal.NInf // Negative infinity +) + // SetLogger set custom log // Use with VoidLogger to disable logging. // If logger is nil, the call is ignored and the existing logger is kept. @@ -215,6 +223,96 @@ func (hs *hooksMixin) processTxPipelineHook(ctx context.Context, cmds []Cmder) e //------------------------------------------------------------------------------ +// Stable identifiers for baseClient.onClose hooks. Each component that +// registers a close callback owns a dedicated id here so the set of known +// hooks is discoverable in one place and id collisions are caught at +// compile time. New ids should be added as additional constants. +const ( + // onCloseHookIDSentinelFailover identifies the close callback installed + // by NewFailoverClient to tear down sentinel failover background work. + onCloseHookIDSentinelFailover = "sentinel-failover" +) + +// onCloseHooks is a small registry of named close callbacks attached to a +// baseClient. Each callback is identified by a stable string id; registering +// the same id twice replaces the previous callback rather than chaining onto +// it. This guarantees the registry stays bounded regardless of how often a +// hook is (re)registered and avoids the unbounded closure chain that +// motivated issue #3772. +// +// Hooks are invoked in registration order. All hooks run regardless of +// individual errors; the first non-nil error is returned. +// +// A zero-value onCloseHooks is ready to use. It is safe for concurrent use. +// Clones of a baseClient share the same *onCloseHooks so registrations and +// close semantics are preserved across WithTimeout / WithContext / etc. +type onCloseHooks struct { + mu sync.Mutex + order []string + hooks map[string]func() error +} + +// register adds or replaces the callback associated with id. Re-registering +// an existing id overwrites the previous callback in place; new ids are +// appended to the invocation order. +func (h *onCloseHooks) register(id string, fn func() error) { + h.mu.Lock() + defer h.mu.Unlock() + if h.hooks == nil { + h.hooks = make(map[string]func() error) + } + if _, exists := h.hooks[id]; !exists { + h.order = append(h.order, id) + } + h.hooks[id] = fn +} + +// unregister removes the callback associated with id, if any. It is kept +// for API symmetry with register so future callers (e.g. dynamic hook +// owners that need to detach before client Close) do not have to +// reinvent it. +// +//nolint:unused // kept for API symmetry with register; see comment above. +func (h *onCloseHooks) unregister(id string) { + h.mu.Lock() + defer h.mu.Unlock() + if _, exists := h.hooks[id]; !exists { + return + } + delete(h.hooks, id) + for i, x := range h.order { + if x == id { + h.order = append(h.order[:i], h.order[i+1:]...) + break + } + } +} + +// run invokes all registered callbacks in registration order and returns +// the first non-nil error encountered. All callbacks are executed even if +// an earlier one returns an error. +func (h *onCloseHooks) run() error { + if h == nil { + return nil + } + h.mu.Lock() + fns := make([]func() error, 0, len(h.order)) + for _, id := range h.order { + if fn := h.hooks[id]; fn != nil { + fns = append(fns, fn) + } + } + h.mu.Unlock() + + var firstErr error + for _, fn := range fns { + if err := fn(); err != nil && firstErr == nil { + firstErr = err + } + } + return firstErr +} + type baseClient struct { opt *Options optLock sync.RWMutex @@ -222,7 +320,13 @@ type baseClient struct { pubSubPool *pool.PubSubPool hooksMixin - onClose func() error // hook called when client is closed + // onClose holds named callbacks invoked when the client is closed. + // Registering a new callback never removes previously registered ones; + // only re-registering the same id replaces the existing callback. This + // lets composing components (e.g. sentinel failover) add close logic + // safely without fear of overwriting each other and without building + // unbounded closure chains on repeated registration. + onClose *onCloseHooks // Push notification processing pushProcessor push.NotificationProcessor @@ -252,8 +356,17 @@ func (c *baseClient) clone() *baseClient { return clone } +// cloneOpt clones c.opt while holding optLock to prevent races with initConn +// which writes to MaintNotificationsConfig.Mode under the same lock. +func (c *baseClient) cloneOpt() *Options { + c.optLock.RLock() + clone := c.opt.clone() + c.optLock.RUnlock() + return clone +} + func (c *baseClient) withTimeout(timeout time.Duration) *baseClient { - opt := c.opt.clone() + opt := c.cloneOpt() opt.ReadTimeout = timeout opt.WriteTimeout = timeout @@ -347,7 +460,11 @@ func (c *baseClient) onAuthenticationErr() func(poolCn *pool.Conn, err error) { if err != nil { if isBadConn(err, false, c.opt.Addr) { // Close the connection to force a reconnection. - err := c.connPool.CloseConn(poolCn) + // Re-auth happens on connections that were idle in the pool (the pool hook + // waits for IDLE state before transitioning to UNUSABLE for re-auth). + // From metrics perspective, the connection was never "used" by a client. + // Note: Using context.Background() as this callback doesn't have access to caller's context. + err := c.connPool.CloseConn(context.Background(), poolCn, pool.CloseReasonAuthError, pool.MetricStateIdle) if err != nil { internal.Logger.Printf(context.Background(), "redis: failed to close connection: %v", err) // try to close the network connection directly @@ -363,27 +480,6 @@ func (c *baseClient) onAuthenticationErr() func(poolCn *pool.Conn, err error) { } } -func (c *baseClient) wrappedOnClose(newOnClose func() error) func() error { - onClose := c.onClose - return func() error { - var firstErr error - err := newOnClose() - // Even if we have an error we would like to execute the onClose hook - // if it exists. We will return the first error that occurred. - // This is to keep error handling consistent with the rest of the code. - if err != nil { - firstErr = err - } - if onClose != nil { - err = onClose() - if err != nil && firstErr == nil { - firstErr = err - } - } - return firstErr - } -} - func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // This function is called in two scenarios: // 1. First-time init: Connection is in CREATED state (from pool.Get()) @@ -483,7 +579,22 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { return fmt.Errorf("failed to subscribe to streaming credentials: %w", initErr) } - c.onClose = c.wrappedOnClose(unsubscribeFromCredentialsProvider) + // Per-connection unsubscribe is attached to the connection itself so it + // runs when this specific connection is closed. Do not register it on + // c.onClose: initConn runs for every (re)initialized connection, and + // attaching per-connection state to the shared baseClient registry would + // either leak entries (one per connection id, never trimmed) or β€” with + // the pre-fix wrappedOnClose approach β€” build an unbounded closure chain + // retaining every prior connection's unsubscribe (see issue #3772). + // + // Note: pool.Conn.SetOnClose OVERWRITES any prior callback (see the + // doc on that method). That is safe here because the streaming + // credentials Manager deduplicates listeners by connection id, so a + // second initConn on the same cn re-Subscribes the SAME listener and + // the returned unsubscribe is equivalent to the one already installed. + // Any future code path that could hand out a distinct unsubscribe on + // re-initialization must first invoke the existing one to avoid + // orphaning the old subscription on the credentials provider. cn.SetOnClose(unsubscribeFromCredentialsProvider) username, password = credentials.BasicAuth() @@ -501,8 +612,13 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { // for redis-server versions that do not support the HELLO command, // RESP2 will continue to be used. + // helloOK tracks whether HELLO succeeded. If it did not, the connection + // falls back to RESP2 regardless of c.opt.Protocol, and features that + // require RESP3 (e.g. maintenance notifications) must be skipped. + helloOK := false if initErr = conn.Hello(ctx, c.opt.Protocol, username, password, c.opt.ClientName).Err(); initErr == nil { // Authentication successful with HELLO command + helloOK = true } else if !isRedisError(initErr) { // When the server responds with the RESP protocol and the result is not a normal // execution result of the HELLO command, we consider it to be an indication that @@ -551,10 +667,38 @@ func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error { maintNotifEnabled := c.opt.MaintNotificationsConfig != nil && c.opt.MaintNotificationsConfig.Mode != maintnotifications.ModeDisabled protocol := c.opt.Protocol var endpointType maintnotifications.EndpointType + var maintNotifMode maintnotifications.Mode if maintNotifEnabled { endpointType = c.opt.MaintNotificationsConfig.EndpointType + maintNotifMode = c.opt.MaintNotificationsConfig.Mode } c.optLock.RUnlock() + + // Maintenance notifications require RESP3 push frames. If HELLO failed + // and the connection fell back to RESP2, there is no point in sending + // CLIENT MAINT_NOTIFICATIONS: the server either rejects it (making the + // error misleading) or accepts it silently, leaving the client unable + // to receive any notifications. Decide based on the actual negotiated + // protocol rather than the requested one. + if maintNotifEnabled && protocol == 3 && !helloOK { + if maintNotifMode == maintnotifications.ModeEnabled { + // Explicitly requested - fail fast with a clear reason. + cn.GetStateMachine().Transition(pool.StateClosed) + if errorCallback := pool.GetMetricErrorCallback(); errorCallback != nil { + errorCallback(ctx, "HANDSHAKE_FAILED", cn, "HANDSHAKE_FAILED", true, 0) + } + return fmt.Errorf("failed to enable maintnotifications: server does not support RESP3 (HELLO command failed)") + } + // auto/other modes: silently disable maintnotifications for this client. + c.optLock.Lock() + c.opt.MaintNotificationsConfig.Mode = maintnotifications.ModeDisabled + c.optLock.Unlock() + if err := c.disableMaintNotificationsUpgrades(); err != nil { + internal.Logger.Printf(ctx, "failed to disable maintnotifications in auto mode: %v", err) + } + maintNotifEnabled = false + } + var maintNotifHandshakeErr error if maintNotifEnabled && protocol == 3 { maintNotifHandshakeErr = conn.ClientMaintNotifications( @@ -703,7 +847,9 @@ func (c *baseClient) process(ctx context.Context, cmd Cmder) error { if cn != nil { lastConn = cn } - if err == nil || !retry { + // Don't retry if command explicitly disables retries (e.g., RawWriteToCmd + // which writes directly to an io.Writer and cannot undo partial writes) + if err == nil || !retry || cmd.NoRetry() { // Record total operation duration if opDurationCallback != nil { operationDuration := time.Since(operationStart) @@ -798,16 +944,9 @@ func classifyCommandError(err error) (errorType, statusCode string, isInternal b } func (c *baseClient) assertUnstableCommand(cmd Cmder) (bool, error) { - switch cmd.(type) { - case *AggregateCmd, *FTInfoCmd, *FTSpellCheckCmd, *FTSearchCmd, *FTSynDumpCmd: - if c.opt.UnstableResp3 { - return true, nil - } else { - return false, fmt.Errorf("RESP3 responses for this command are disabled because they may still change. Please set the flag UnstableResp3. See the README and the release notes for guidance") - } - default: - return false, nil - } + // All search commands (FTSearchCmd, AggregateCmd, FTInfoCmd, FTSpellCheckCmd, FTSynDumpCmd) + // now have stable RESP3 parsing. No commands require the UnstableResp3 flag anymore. + return false, nil } func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, *pool.Conn, error) { @@ -948,10 +1087,8 @@ func (c *baseClient) Close() error { firstErr = err } - if c.onClose != nil { - if err := c.onClose(); err != nil && firstErr == nil { - firstErr = err - } + if err := c.onClose.run(); err != nil && firstErr == nil { + firstErr = err } // Unregister pools from OTel before closing them @@ -1028,7 +1165,10 @@ func (c *baseClient) generalProcessPipeline( canRetry, err = p(ctx, cn, cmds) return err }) - if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) { + // Don't retry if any command in the pipeline explicitly disables retries + // (e.g., RawWriteToCmd which writes directly to an io.Writer and cannot + // undo partial writes on retry) + if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) || cmdsContainNoRetry(cmds) { // The error should be set here only when failing to obtain the conn. if !isRedisError(lastErr) { setCmdsErr(cmds, lastErr) @@ -1196,6 +1336,7 @@ type Client struct { } // NewClient returns a client to the Redis Server specified by Options. +// Passing nil Options will cause a panic. func NewClient(opt *Options) *Client { if opt == nil { panic("redis: NewClient nil options") @@ -1208,7 +1349,8 @@ func NewClient(opt *Options) *Client { c := Client{ baseClient: &baseClient{ - opt: opt, + opt: opt, + onClose: &onCloseHooks{}, }, } c.init() @@ -1295,7 +1437,8 @@ func (c *Client) Process(ctx context.Context, cmd Cmder) error { return err } -// Options returns read-only Options that were used to create the client. +// Options returns read-only *Options that were used to create the client. +// Any alteration of the returned *Options may result in undefined behaviour. func (c *Client) Options() *Options { return c.opt } @@ -1490,6 +1633,7 @@ func newConn(opt *Options, connPool pool.Pooler, parentHooks *hooksMixin) *Conn baseClient: baseClient{ opt: opt, connPool: connPool, + onClose: &onCloseHooks{}, }, } diff --git a/vendor/github.com/redis/go-redis/v9/ring.go b/vendor/github.com/redis/go-redis/v9/ring.go index d9220ddb..b60d3eab 100644 --- a/vendor/github.com/redis/go-redis/v9/ring.go +++ b/vendor/github.com/redis/go-redis/v9/ring.go @@ -5,21 +5,18 @@ import ( "crypto/tls" "errors" "fmt" + "math/rand" "net" "strconv" "sync" "sync/atomic" "time" - "github.com/cespare/xxhash/v2" - "github.com/dgryski/go-rendezvous" //nolint "github.com/redis/go-redis/v9/auth" - "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/hashtag" "github.com/redis/go-redis/v9/internal/pool" "github.com/redis/go-redis/v9/internal/proto" - "github.com/redis/go-redis/v9/internal/rand" ) var errRingShardsDown = errors.New("redis: all ring shards are down") @@ -36,16 +33,8 @@ type ConsistentHash interface { Get(string) string } -type rendezvousWrapper struct { - *rendezvous.Rendezvous -} - -func (w rendezvousWrapper) Get(key string) string { - return w.Lookup(key) -} - func newRendezvous(shards []string) ConsistentHash { - return rendezvousWrapper{rendezvous.New(shards, xxhash.Sum64String)} + return hashtag.NewRendezvousHash(shards) } //------------------------------------------------------------------------------ @@ -120,6 +109,10 @@ type RingOptions struct { // default: 100 milliseconds DialerRetryTimeout time.Duration + // DialerRetryBackoff controls the delay between dial retry attempts. + // See Options.DialerRetryBackoff for details. + DialerRetryBackoff func(attempt int) time.Duration + ReadTimeout time.Duration WriteTimeout time.Duration ContextTimeoutEnabled bool @@ -165,7 +158,11 @@ type RingOptions struct { // default: false DisableIdentity bool IdentitySuffix string - UnstableResp3 bool + + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. + UnstableResp3 bool } func (opt *RingOptions) init() { @@ -233,6 +230,7 @@ func (opt *RingOptions) clientOptions() *Options { DialTimeout: opt.DialTimeout, DialerRetries: opt.DialerRetries, DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, ContextTimeoutEnabled: opt.ContextTimeoutEnabled, @@ -600,6 +598,8 @@ type Ring struct { heartbeatCancelFn context.CancelFunc } +// NewRing returns a Redis Ring client to the Redis Server specified by RingOptions. +// Passing nil RingOptions will cause a panic. func NewRing(opt *RingOptions) *Ring { if opt == nil { panic("redis: NewRing nil options") @@ -642,7 +642,8 @@ func (c *Ring) Process(ctx context.Context, cmd Cmder) error { return err } -// Options returns read-only Options that were used to create the client. +// Options returns read-only *RingOptions that were used to create the client. +// Any alteration of the returned *RingOptions may result in undefined behaviour. func (c *Ring) Options() *RingOptions { return c.opt } @@ -774,7 +775,10 @@ func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) { } func (c *Ring) cmdShard(cmd Cmder) (*ringShard, error) { - pos := cmdFirstKeyPos(cmd) + // TODO: populate cmdsInfoCache lazily (via cmdsInfoCache.Get) so that + // the warm-cache branch in cmdFirstKeyPosWithInfo is reachable for Ring, + // mirroring how ClusterClient.cmdInfo works. For now pass nil + pos := cmdFirstKeyPosWithInfo(cmd, nil) if pos == 0 { return c.sharding.Random() } @@ -797,7 +801,7 @@ func (c *Ring) process(ctx context.Context, cmd Cmder) error { } lastErr = shard.Client.Process(ctx, cmd) - if lastErr == nil || !shouldRetry(lastErr, cmd.readTimeout() == nil) { + if lastErr == nil || !shouldRetry(lastErr, cmd.readTimeout() == nil) || cmd.NoRetry() { return lastErr } } @@ -842,7 +846,7 @@ func (c *Ring) generalProcessPipeline( cmdsMap := make(map[string][]Cmder) for _, cmd := range cmds { - hash := cmd.stringArg(cmdFirstKeyPos(cmd)) + hash := cmd.stringArg(cmdFirstKeyPosWithInfo(cmd, nil)) if hash != "" { hash = c.sharding.Hash(hash) } diff --git a/vendor/github.com/redis/go-redis/v9/script.go b/vendor/github.com/redis/go-redis/v9/script.go index 626ab03b..92d508f9 100644 --- a/vendor/github.com/redis/go-redis/v9/script.go +++ b/vendor/github.com/redis/go-redis/v9/script.go @@ -4,7 +4,9 @@ import ( "context" "crypto/sha1" "encoding/hex" + "errors" "io" + "sync" ) type Scripter interface { @@ -23,28 +25,69 @@ var ( ) type Script struct { - src, hash string + src string + mu sync.RWMutex + hash string + serverSHA bool // if true: do not compute SHA-1 in Go; load digest from Redis (SCRIPT LOAD) } func NewScript(src string) *Script { h := sha1.New() _, _ = io.WriteString(h, src) + + return &Script{ + src: src, + hash: hex.EncodeToString(h.Sum(nil)), + serverSHA: false, + } +} + +// NewScriptServerSHA creates a Script that avoids computing SHA-1 in Go. +// The digest is obtained from Redis via SCRIPT LOAD (server-side hashing), +// then EVALSHA/EVALSHA_RO is used. +func NewScriptServerSHA(src string) *Script { return &Script{ - src: src, - hash: hex.EncodeToString(h.Sum(nil)), + src: src, + serverSHA: true, } } func (s *Script) Hash() string { + s.mu.RLock() + defer s.mu.RUnlock() return s.hash } func (s *Script) Load(ctx context.Context, c Scripter) *StringCmd { - return c.ScriptLoad(ctx, s.src) + cmd := c.ScriptLoad(ctx, s.src) + if err := cmd.Err(); err == nil { + s.mu.Lock() + s.hash = cmd.Val() + s.mu.Unlock() + } + return cmd } func (s *Script) Exists(ctx context.Context, c Scripter) *BoolSliceCmd { - return c.ScriptExists(ctx, s.hash) + s.mu.RLock() + hash := s.hash + serverSHA := s.serverSHA + s.mu.RUnlock() + if hash == "" && serverSHA { + // For server-side scripts, obtain digest from Redis first. + // If hash is empty, it means SCRIPT LOAD was not called yet, so we check existence of empty hash which will return false. + // This avoids unnecessary SCRIPT LOAD just to check existence. + if err := s.ensureHash(ctx, c); err != nil { + return c.ScriptExists(ctx, "") + } + s.mu.RLock() + hash = s.hash + s.mu.RUnlock() + } + if hash == "" { + return c.ScriptExists(ctx, "") + } + return c.ScriptExists(ctx, hash) } func (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { @@ -55,19 +98,101 @@ func (s *Script) EvalRO(ctx context.Context, c Scripter, keys []string, args ... return c.EvalRO(ctx, s.src, keys, args...) } +// ensureHash ensures that s.hash is populated by using SCRIPT LOAD. +// It never calls SHA-1 in Go; Redis computes and returns the digest. +func (s *Script) ensureHash(ctx context.Context, c Scripter) error { + // Fast path: read lock, return if hash is already set. + s.mu.RLock() + if s.hash != "" { + s.mu.RUnlock() + return nil + } + s.mu.RUnlock() + + // Slow path: acquire write lock and load. + s.mu.Lock() + if s.hash != "" { + s.mu.Unlock() + return nil + } + cmd := c.ScriptLoad(ctx, s.src) + if err := cmd.Err(); err != nil { + s.mu.Unlock() + return err + } + s.hash = cmd.Val() + s.mu.Unlock() + return nil +} + func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { - return c.EvalSha(ctx, s.hash, keys, args...) + // Default behavior: use client-side SHA-1 computed in NewScript. + if !s.serverSHA { + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + return c.EvalSha(ctx, hash, keys, args...) + } + + // Server-side SHA via SCRIPT LOAD + EVALSHA. + if err := s.ensureHash(ctx, c); err != nil { + return s.Eval(ctx, c, keys, args...) + } + + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + + r := c.EvalSha(ctx, hash, keys, args...) + if HasErrorPrefix(r.Err(), "NOSCRIPT") { + // Script cache was flushed; reload and retry once. + if err := s.ensureHash(ctx, c); err != nil { + return s.Eval(ctx, c, keys, args...) + } + s.mu.RLock() + hash = s.hash + s.mu.RUnlock() + return c.EvalSha(ctx, hash, keys, args...) + } + + return r } func (s *Script) EvalShaRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { - return c.EvalShaRO(ctx, s.hash, keys, args...) + if !s.serverSHA { + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + return c.EvalShaRO(ctx, hash, keys, args...) + } + + if err := s.ensureHash(ctx, c); err != nil { + return s.EvalRO(ctx, c, keys, args...) + } + + s.mu.RLock() + hash := s.hash + s.mu.RUnlock() + + r := c.EvalShaRO(ctx, hash, keys, args...) + if HasErrorPrefix(r.Err(), "NOSCRIPT") { + if err := s.ensureHash(ctx, c); err != nil { + return s.EvalRO(ctx, c, keys, args...) + } + s.mu.RLock() + hash = s.hash + s.mu.RUnlock() + return c.EvalShaRO(ctx, hash, keys, args...) + } + + return r } // Run optimistically uses EVALSHA to run the script. If script does not exist // it is retried using EVAL. func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { r := s.EvalSha(ctx, c, keys, args...) - if HasErrorPrefix(r.Err(), "NOSCRIPT") { + if errors.Is(r.Err(), ErrNoScript) { return s.Eval(ctx, c, keys, args...) } return r @@ -77,7 +202,7 @@ func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...int // it is retried using EVAL_RO. func (s *Script) RunRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd { r := s.EvalShaRO(ctx, c, keys, args...) - if HasErrorPrefix(r.Err(), "NOSCRIPT") { + if errors.Is(r.Err(), ErrNoScript) { return s.EvalRO(ctx, c, keys, args...) } return r diff --git a/vendor/github.com/redis/go-redis/v9/scripting_commands.go b/vendor/github.com/redis/go-redis/v9/scripting_commands.go index af9c3397..3310b9d0 100644 --- a/vendor/github.com/redis/go-redis/v9/scripting_commands.go +++ b/vendor/github.com/redis/go-redis/v9/scripting_commands.go @@ -60,6 +60,11 @@ func (c cmdable) eval(ctx context.Context, name, payload string, keys []string, cmd.SetFirstKeyPos(3) } _ = c(ctx, cmd) + if err := cmd.Err(); err != nil { + if HasErrorPrefix(err, "NOSCRIPT") { + cmd.SetErr(ErrNoScript) + } + } return cmd } diff --git a/vendor/github.com/redis/go-redis/v9/search_builders.go b/vendor/github.com/redis/go-redis/v9/search_builders.go index 91f06340..a6c6718c 100644 --- a/vendor/github.com/redis/go-redis/v9/search_builders.go +++ b/vendor/github.com/redis/go-redis/v9/search_builders.go @@ -2,6 +2,7 @@ package redis import ( "context" + "fmt" ) // ---------------------- @@ -215,6 +216,7 @@ type AggregateBuilder struct { index string query string options *FTAggregateOptions + err error } // NewAggregateBuilder creates a new AggregateBuilder for FT.AGGREGATE commands. @@ -223,6 +225,14 @@ func (c *Client) NewAggregateBuilder(ctx context.Context, index, query string) * return &AggregateBuilder{c: c, ctx: ctx, index: index, query: query, options: &FTAggregateOptions{LimitOffset: -1}} } +// setErr records the first error produced while building the pipeline. +// Subsequent errors are ignored; the first error is returned from Run. +func (b *AggregateBuilder) setErr(err error) { + if b.err == nil { + b.err = err + } +} + // Verbatim includes VERBATIM. func (b *AggregateBuilder) Verbatim() *AggregateBuilder { b.options.Verbatim = true; return b } @@ -241,15 +251,15 @@ func (b *AggregateBuilder) LoadAll() *AggregateBuilder { return b } -// Load adds LOAD [AS alias]... -// You can call it multiple times for multiple fields. +// Load adds a LOAD [AS alias] step. +// You can call it multiple times; each call becomes a separate LOAD clause +// at its position in the pipeline. func (b *AggregateBuilder) Load(field string, alias ...string) *AggregateBuilder { - // each Load entry becomes one element in options.Load - l := FTAggregateLoad{Field: field} + l := &FTAggregateLoad{Field: field} if len(alias) > 0 { l.As = alias[0] } - b.options.Load = append(b.options.Load, l) + b.options.Steps = append(b.options.Steps, FTAggregateStep{Load: l}) return b } @@ -259,62 +269,79 @@ func (b *AggregateBuilder) Timeout(ms int) *AggregateBuilder { return b } -// Apply adds APPLY [AS alias]. +// Apply adds an APPLY [AS alias] step. func (b *AggregateBuilder) Apply(field string, alias ...string) *AggregateBuilder { - a := FTAggregateApply{Field: field} + a := &FTAggregateApply{Field: field} if len(alias) > 0 { a.As = alias[0] } - b.options.Apply = append(b.options.Apply, a) + b.options.Steps = append(b.options.Steps, FTAggregateStep{Apply: a}) return b } -// GroupBy starts a new GROUPBY clause. +// GroupBy adds a new GROUPBY step. func (b *AggregateBuilder) GroupBy(fields ...interface{}) *AggregateBuilder { - b.options.GroupBy = append(b.options.GroupBy, FTAggregateGroupBy{ - Fields: fields, + b.options.Steps = append(b.options.Steps, FTAggregateStep{ + GroupBy: &FTAggregateGroupBy{Fields: fields}, }) return b } -// Reduce adds a REDUCE [<#args> ] clause to the *last* GROUPBY. +// Reduce adds a REDUCE [<#args> ] clause to the last step, +// which must be a GROUPBY. If it is not, Run will return an error. func (b *AggregateBuilder) Reduce(fn SearchAggregator, args ...interface{}) *AggregateBuilder { - if len(b.options.GroupBy) == 0 { - // no GROUPBY yet β€” nothing to attach to + n := len(b.options.Steps) + if n == 0 || b.options.Steps[n-1].GroupBy == nil { + b.setErr(fmt.Errorf("FT.AGGREGATE: Reduce must follow a GroupBy step")) return b } - idx := len(b.options.GroupBy) - 1 - b.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{ - Reducer: fn, - Args: args, - }) + g := b.options.Steps[n-1].GroupBy + g.Reduce = append(g.Reduce, FTAggregateReducer{Reducer: fn, Args: args}) return b } -// ReduceAs does the same but also sets an alias: REDUCE … AS +// ReduceAs does the same but also sets an alias: REDUCE … AS . +// The last step must be a GROUPBY; otherwise Run will return an error. func (b *AggregateBuilder) ReduceAs(fn SearchAggregator, alias string, args ...interface{}) *AggregateBuilder { - if len(b.options.GroupBy) == 0 { + n := len(b.options.Steps) + if n == 0 || b.options.Steps[n-1].GroupBy == nil { + b.setErr(fmt.Errorf("FT.AGGREGATE: ReduceAs must follow a GroupBy step")) return b } - idx := len(b.options.GroupBy) - 1 - b.options.GroupBy[idx].Reduce = append(b.options.GroupBy[idx].Reduce, FTAggregateReducer{ - Reducer: fn, - Args: args, - As: alias, - }) + g := b.options.Steps[n-1].GroupBy + g.Reduce = append(g.Reduce, FTAggregateReducer{Reducer: fn, Args: args, As: alias}) return b } -// SortBy adds SORTBY ASC|DESC. +// SortBy adds SORTBY ASC|DESC. Consecutive SortBy calls (with no +// other step in between) are merged into a single SORTBY clause so fields +// act as tiebreakers. A SortBy call after a non-SortBy step starts a new +// SORTBY step. +// +// Note: this is a semantics change from earlier experimental versions of +// the builder, where SortBy always accumulated into a single SORTBY clause +// regardless of position in the pipeline. func (b *AggregateBuilder) SortBy(field string, asc bool) *AggregateBuilder { sb := FTAggregateSortBy{FieldName: field, Asc: asc, Desc: !asc} - b.options.SortBy = append(b.options.SortBy, sb) + if n := len(b.options.Steps); n > 0 && b.options.Steps[n-1].SortBy != nil { + b.options.Steps[n-1].SortBy.Fields = append(b.options.Steps[n-1].SortBy.Fields, sb) + return b + } + b.options.Steps = append(b.options.Steps, FTAggregateStep{ + SortBy: &FTAggregateSortByStep{Fields: []FTAggregateSortBy{sb}}, + }) return b } -// SortByMax sets MAX (only if SortBy was called). +// SortByMax sets MAX on the last SORTBY step. The last step must be a +// SORTBY; otherwise Run will return an error. func (b *AggregateBuilder) SortByMax(max int) *AggregateBuilder { - b.options.SortByMax = max + n := len(b.options.Steps) + if n == 0 || b.options.Steps[n-1].SortBy == nil { + b.setErr(fmt.Errorf("FT.AGGREGATE: SortByMax must follow a SortBy step")) + return b + } + b.options.Steps[n-1].SortBy.Max = max return b } @@ -352,8 +379,14 @@ func (b *AggregateBuilder) Dialect(version int) *AggregateBuilder { return b } -// Run executes FT.AGGREGATE and returns a typed result. +// Run executes FT.AGGREGATE and returns a typed result. If the builder +// recorded a validation error while constructing the pipeline (for example, +// calling SortByMax when the last step is not a SortBy), that error is +// returned without issuing the command. func (b *AggregateBuilder) Run() (*FTAggregateResult, error) { + if b.err != nil { + return nil, b.err + } cmd := b.c.FTAggregateWithArgs(b.ctx, b.index, b.query, b.options) return cmd.Result() } diff --git a/vendor/github.com/redis/go-redis/v9/search_commands.go b/vendor/github.com/redis/go-redis/v9/search_commands.go index 0fef8ffc..b13aa5be 100644 --- a/vendor/github.com/redis/go-redis/v9/search_commands.go +++ b/vendor/github.com/redis/go-redis/v9/search_commands.go @@ -3,7 +3,10 @@ package redis import ( "context" "fmt" + "maps" + "slices" "strconv" + "strings" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/proto" @@ -256,22 +259,42 @@ type FTAggregateWithCursor struct { MaxIdle int } +// FTAggregateSortByStep represents a SORTBY operation with optional MAX. +// Used inside FTAggregateStep to place SORTBY at an arbitrary position in +// the aggregation pipeline. +type FTAggregateSortByStep struct { + Fields []FTAggregateSortBy + Max int // 0 means no MAX +} + +// FTAggregateStep represents a single operation in the aggregation pipeline. +// LOAD, APPLY, SORTBY and GROUPBY can all appear multiple times in any order. +// Exactly one of the fields should be set per step. +type FTAggregateStep struct { + Load *FTAggregateLoad + Apply *FTAggregateApply + GroupBy *FTAggregateGroupBy + SortBy *FTAggregateSortByStep +} + type FTAggregateOptions struct { - Verbatim bool - LoadAll bool - Load []FTAggregateLoad - Timeout int - GroupBy []FTAggregateGroupBy - SortBy []FTAggregateSortBy - SortByMax int + Verbatim bool + LoadAll bool + Timeout int // Scorer is used to set scoring function, if not set passed, a default will be used. // The default scorer depends on the Redis version: // - `BM25` for Redis >= 8 // - `TFIDF` for Redis < 8 Scorer string // AddScores is available in Redis CE 8 - AddScores bool - Apply []FTAggregateApply + AddScores bool + + // Steps is the ordered sequence of aggregation pipeline operations. + // It can contain LOAD, APPLY, GROUPBY and SORTBY in any order, multiple times. + // Steps cannot be combined with the deprecated Load, Apply, GroupBy, SortBy + // and SortByMax fields: doing so returns an error. + Steps []FTAggregateStep + LimitOffset int Limit int Filter string @@ -280,6 +303,17 @@ type FTAggregateOptions struct { Params map[string]interface{} // Dialect 1,3 and 4 are deprecated since redis 8.0 DialectVersion int + + // Deprecated: Use Steps instead. + Load []FTAggregateLoad + // Deprecated: Use Steps instead. + GroupBy []FTAggregateGroupBy + // Deprecated: Use Steps instead. + SortBy []FTAggregateSortBy + // Deprecated: Use Steps instead. + SortByMax int + // Deprecated: Use Steps instead. + Apply []FTAggregateApply } type FTSearchFilter struct { @@ -374,11 +408,13 @@ const ( type FTHybridVectorExpression struct { VectorField string VectorData Vector - // VectorParamName specifies the parameter name for passing vector data via PARAMS mechanism. - // REQUIRED for Redis 8.6+ (inline vector blobs are not supported in 8.6+). - // Optional for Redis 8.4-8.5 (both inline and PARAMS are supported). - // When set, the vector blob will be passed as: VSIM @field $VectorParamName PARAMS ... VectorParamName - // When empty, the vector blob will be inlined: VSIM @field (fails on Redis 8.6+) + // VectorParamName optionally specifies the parameter name used to pass the + // vector data via the PARAMS mechanism. + // Vector data is always passed via PARAMS because inline vector blobs are no + // longer supported by Redis. When left empty, the library generates a unique + // parameter name automatically (e.g. "__vector_param_0") without mutating + // FTHybridOptions.Params and without colliding with any explicit names. + // The vector blob is passed as: VSIM @field $VectorParamName ... PARAMS ... VectorParamName VectorParamName string Method FTHybridVectorMethod MethodParams []interface{} @@ -448,9 +484,12 @@ type FTSynDumpCmd struct { val []FTSynDumpResult } +// FTAggregateResult represents the result of an aggregate operation +// NOTE: For RESP3 Total is not reliable (before Redis 8.8) type FTAggregateResult struct { - Total int - Rows []AggregateRow + Total int + Rows []AggregateRow + Warnings []string } type AggregateRow struct { @@ -580,8 +619,9 @@ type SpellCheckSuggestion struct { } type FTSearchResult struct { - Total int - Docs []Document + Total int + Docs []Document + Warnings []string } type Document struct { @@ -615,9 +655,112 @@ func (c cmdable) FTAggregate(ctx context.Context, index string, query string) *M return cmd } +// validateFTAggregateOptions validates mutually exclusive combinations of +// FTAggregateOptions fields before any command arguments are constructed. +func validateFTAggregateOptions(options *FTAggregateOptions) error { + if len(options.Steps) > 0 { + if options.Load != nil || options.Apply != nil || options.GroupBy != nil || + options.SortBy != nil || options.SortByMax != 0 { + return fmt.Errorf("FT.AGGREGATE: Steps cannot be combined with the deprecated Load, Apply, GroupBy, SortBy and SortByMax fields") + } + if options.LoadAll { + for _, step := range options.Steps { + if step.Load != nil { + return fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + } + } + } + if options.LoadAll && options.Load != nil { + return fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") + } + return nil +} + +// appendFTAggregateStep appends the Redis command arguments for a single +// aggregation pipeline step. Each step must set exactly one of Load, Apply, +// GroupBy or SortBy. +func appendFTAggregateStep(args []interface{}, step FTAggregateStep) ([]interface{}, error) { + set := 0 + if step.Load != nil { + set++ + } + if step.Apply != nil { + set++ + } + if step.GroupBy != nil { + set++ + } + if step.SortBy != nil { + set++ + } + if set != 1 { + return args, fmt.Errorf("FT.AGGREGATE: each step must set exactly one of Load, Apply, GroupBy, SortBy (got %d)", set) + } + + switch { + case step.Load != nil: + args = append(args, "LOAD") + countIdx := len(args) + args = append(args, 0) + count := 0 + args = append(args, step.Load.Field) + count++ + if step.Load.As != "" { + args = append(args, "AS", step.Load.As) + count += 2 + } + args[countIdx] = count + case step.Apply != nil: + args = append(args, "APPLY", step.Apply.Field) + if step.Apply.As != "" { + args = append(args, "AS", step.Apply.As) + } + case step.GroupBy != nil: + args = append(args, "GROUPBY", len(step.GroupBy.Fields)) + args = append(args, step.GroupBy.Fields...) + for _, reducer := range step.GroupBy.Reduce { + args = append(args, "REDUCE", reducer.Reducer.String()) + if reducer.Args != nil { + args = append(args, len(reducer.Args)) + args = append(args, reducer.Args...) + } else { + args = append(args, 0) + } + if reducer.As != "" { + args = append(args, "AS", reducer.As) + } + } + case step.SortBy != nil: + args = append(args, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range step.SortBy.Fields { + if sortBy.Asc && sortBy.Desc { + return args, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + args = append(args, len(sortByOptions)) + args = append(args, sortByOptions...) + if step.SortBy.Max > 0 { + args = append(args, "MAX", step.SortBy.Max) + } + } + return args, nil +} + func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery, error) { queryArgs := []interface{}{query} if options != nil { + if err := validateFTAggregateOptions(options); err != nil { + return nil, err + } if options.Verbatim { queryArgs = append(queryArgs, "VERBATIM") } @@ -630,13 +773,10 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery queryArgs = append(queryArgs, "ADDSCORES") } - if options.LoadAll && options.Load != nil { - return nil, fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive") - } if options.LoadAll { queryArgs = append(queryArgs, "LOAD", "*") } - if options.Load != nil { + if len(options.Steps) == 0 && options.Load != nil { queryArgs = append(queryArgs, "LOAD", len(options.Load)) index, count := len(queryArgs)-1, 0 for _, load := range options.Load { @@ -654,53 +794,63 @@ func FTAggregateQuery(query string, options *FTAggregateOptions) (AggregateQuery queryArgs = append(queryArgs, "TIMEOUT", options.Timeout) } - for _, apply := range options.Apply { - queryArgs = append(queryArgs, "APPLY", apply.Field) - if apply.As != "" { - queryArgs = append(queryArgs, "AS", apply.As) + if len(options.Steps) > 0 { + for _, step := range options.Steps { + var err error + queryArgs, err = appendFTAggregateStep(queryArgs, step) + if err != nil { + return nil, err + } + } + } else { + for _, apply := range options.Apply { + queryArgs = append(queryArgs, "APPLY", apply.Field) + if apply.As != "" { + queryArgs = append(queryArgs, "AS", apply.As) + } } - } - if options.GroupBy != nil { - for _, groupBy := range options.GroupBy { - queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) - queryArgs = append(queryArgs, groupBy.Fields...) - - for _, reducer := range groupBy.Reduce { - queryArgs = append(queryArgs, "REDUCE") - queryArgs = append(queryArgs, reducer.Reducer.String()) - if reducer.Args != nil { - queryArgs = append(queryArgs, len(reducer.Args)) - queryArgs = append(queryArgs, reducer.Args...) - } else { - queryArgs = append(queryArgs, 0) - } - if reducer.As != "" { - queryArgs = append(queryArgs, "AS", reducer.As) + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + queryArgs = append(queryArgs, "GROUPBY", len(groupBy.Fields)) + queryArgs = append(queryArgs, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + queryArgs = append(queryArgs, "REDUCE") + queryArgs = append(queryArgs, reducer.Reducer.String()) + if reducer.Args != nil { + queryArgs = append(queryArgs, len(reducer.Args)) + queryArgs = append(queryArgs, reducer.Args...) + } else { + queryArgs = append(queryArgs, 0) + } + if reducer.As != "" { + queryArgs = append(queryArgs, "AS", reducer.As) + } } } } - } - if options.SortBy != nil { - queryArgs = append(queryArgs, "SORTBY") - sortByOptions := []interface{}{} - for _, sortBy := range options.SortBy { - sortByOptions = append(sortByOptions, sortBy.FieldName) - if sortBy.Asc && sortBy.Desc { - return nil, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive") - } - if sortBy.Asc { - sortByOptions = append(sortByOptions, "ASC") - } - if sortBy.Desc { - sortByOptions = append(sortByOptions, "DESC") + if options.SortBy != nil { + queryArgs = append(queryArgs, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + return nil, fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive") + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } } + queryArgs = append(queryArgs, len(sortByOptions)) + queryArgs = append(queryArgs, sortByOptions...) + } + if options.SortByMax > 0 { + queryArgs = append(queryArgs, "MAX", options.SortByMax) } - queryArgs = append(queryArgs, len(sortByOptions)) - queryArgs = append(queryArgs, sortByOptions...) - } - if options.SortByMax > 0 { - queryArgs = append(queryArgs, "MAX", options.SortByMax) } if options.LimitOffset >= 0 && options.Limit > 0 { queryArgs = append(queryArgs, "LIMIT", options.LimitOffset, options.Limit) @@ -806,15 +956,129 @@ func (cmd *AggregateCmd) String() string { } func (cmd *AggregateCmd) readReply(rd *proto.Reader) (err error) { - data, err := rd.ReadSlice() + readType, err := rd.PeekReplyType() if err != nil { return err } - cmd.val, err = ProcessAggregateResult(data) + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + // Parse the raw response into structured result + if mapVal, ok := cmd.rawVal.(map[interface{}]interface{}); ok { + cmd.val, err = parseFTAggregateMapRESP3(mapVal) + } else { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + return err + } + + // RESP2 format or error response - use ReadReply to handle errors properly + data, err := rd.ReadReply() if err != nil { return err } - return nil + cmd.rawVal = data // Store raw value for debugging + if dataSlice, ok := data.([]interface{}); ok { + cmd.val, err = ProcessAggregateResult(dataSlice) + return err + } + return fmt.Errorf("unexpected response type: %T", data) +} + +// parseFTAggregateMapRESP3 parses the RESP3 format response from FT.AGGREGATE. +// It takes a map[interface{}]interface{} which is the raw response from ReadReply(). +// RESP3 format: +// +// %5 +// $10 attributes => *0 +// $13 total_results => :N +// $6 format => $6 STRING +// $7 results => *N (array of maps with extra_attributes, values) +// $7 warning => *N (array of strings) +func parseFTAggregateMapRESP3(data map[interface{}]interface{}) (*FTAggregateResult, error) { + result := &FTAggregateResult{ + Rows: make([]AggregateRow, 0), + } + + for k, v := range data { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "total_results": + result.Total = internal.ToInteger(v) + case "results": + if resultsData, ok := v.([]interface{}); ok { + rows, err := parseFTAggregateResultsMapRESP3(resultsData) + if err != nil { + return nil, err + } + result.Rows = rows + } + case "warning": + if warningsData, ok := v.([]interface{}); ok { + result.Warnings = make([]string, 0, len(warningsData)) + for _, w := range warningsData { + if ws, ok := w.(string); ok { + result.Warnings = append(result.Warnings, ws) + } + } + } + // Ignore "attributes", "format", and other fields as per the spec + } + } + + return result, nil +} + +// parseFTAggregateResultsMapRESP3 parses the results array from RESP3 FT.AGGREGATE response. +func parseFTAggregateResultsMapRESP3(resultsData []interface{}) ([]AggregateRow, error) { + rows := make([]AggregateRow, 0, len(resultsData)) + for _, item := range resultsData { + if itemMap, ok := item.(map[interface{}]interface{}); ok { + row, err := parseFTAggregateRowMapRESP3(itemMap) + if err != nil { + return nil, err + } + rows = append(rows, row) + } + } + return rows, nil +} + +// parseFTAggregateRowMapRESP3 parses a single row from RESP3 FT.AGGREGATE response. +func parseFTAggregateRowMapRESP3(itemMap map[interface{}]interface{}) (AggregateRow, error) { + row := AggregateRow{ + Fields: make(map[string]interface{}), + } + + for k, v := range itemMap { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "extra_attributes": + if extraAttrs, ok := v.(map[interface{}]interface{}); ok { + for ek, ev := range extraAttrs { + if ekStr, ok := ek.(string); ok { + row.Fields[ekStr] = ev + } + } + } + // Ignore "values" and other fields as per the spec + } + } + + return row, nil } func (cmd *AggregateCmd) Clone() Cmder { @@ -835,6 +1099,10 @@ func (cmd *AggregateCmd) Clone() Cmder { } } } + if cmd.val.Warnings != nil { + val.Warnings = make([]string, len(cmd.val.Warnings)) + copy(val.Warnings, cmd.val.Warnings) + } } return &AggregateCmd{ baseCmd: cmd.cloneBaseCmd(), @@ -850,6 +1118,11 @@ func (cmd *AggregateCmd) Clone() Cmder { func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query string, options *FTAggregateOptions) *AggregateCmd { args := []interface{}{"FT.AGGREGATE", index, query} if options != nil { + if err := validateFTAggregateOptions(options); err != nil { + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } if options.Verbatim { args = append(args, "VERBATIM") } @@ -859,15 +1132,10 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.AddScores { args = append(args, "ADDSCORES") } - if options.LoadAll && options.Load != nil { - cmd := NewAggregateCmd(ctx, args...) - cmd.SetErr(fmt.Errorf("FT.AGGREGATE: LOADALL and LOAD are mutually exclusive")) - return cmd - } if options.LoadAll { args = append(args, "LOAD", "*") } - if options.Load != nil { + if len(options.Steps) == 0 && options.Load != nil { args = append(args, "LOAD", len(options.Load)) index, count := len(args)-1, 0 for _, load := range options.Load { @@ -883,54 +1151,66 @@ func (c cmdable) FTAggregateWithArgs(ctx context.Context, index string, query st if options.Timeout > 0 { args = append(args, "TIMEOUT", options.Timeout) } - for _, apply := range options.Apply { - args = append(args, "APPLY", apply.Field) - if apply.As != "" { - args = append(args, "AS", apply.As) - } - } - if options.GroupBy != nil { - for _, groupBy := range options.GroupBy { - args = append(args, "GROUPBY", len(groupBy.Fields)) - args = append(args, groupBy.Fields...) - - for _, reducer := range groupBy.Reduce { - args = append(args, "REDUCE") - args = append(args, reducer.Reducer.String()) - if reducer.Args != nil { - args = append(args, len(reducer.Args)) - args = append(args, reducer.Args...) - } else { - args = append(args, 0) - } - if reducer.As != "" { - args = append(args, "AS", reducer.As) - } - } - } - } - if options.SortBy != nil { - args = append(args, "SORTBY") - sortByOptions := []interface{}{} - for _, sortBy := range options.SortBy { - sortByOptions = append(sortByOptions, sortBy.FieldName) - if sortBy.Asc && sortBy.Desc { + if len(options.Steps) > 0 { + for _, step := range options.Steps { + var err error + args, err = appendFTAggregateStep(args, step) + if err != nil { cmd := NewAggregateCmd(ctx, args...) - cmd.SetErr(fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive")) + cmd.SetErr(err) return cmd } - if sortBy.Asc { - sortByOptions = append(sortByOptions, "ASC") + } + } else { + for _, apply := range options.Apply { + args = append(args, "APPLY", apply.Field) + if apply.As != "" { + args = append(args, "AS", apply.As) } - if sortBy.Desc { - sortByOptions = append(sortByOptions, "DESC") + } + if options.GroupBy != nil { + for _, groupBy := range options.GroupBy { + args = append(args, "GROUPBY", len(groupBy.Fields)) + args = append(args, groupBy.Fields...) + + for _, reducer := range groupBy.Reduce { + args = append(args, "REDUCE") + args = append(args, reducer.Reducer.String()) + if reducer.Args != nil { + args = append(args, len(reducer.Args)) + args = append(args, reducer.Args...) + } else { + args = append(args, 0) + } + if reducer.As != "" { + args = append(args, "AS", reducer.As) + } + } } } - args = append(args, len(sortByOptions)) - args = append(args, sortByOptions...) - } - if options.SortByMax > 0 { - args = append(args, "MAX", options.SortByMax) + if options.SortBy != nil { + args = append(args, "SORTBY") + sortByOptions := []interface{}{} + for _, sortBy := range options.SortBy { + sortByOptions = append(sortByOptions, sortBy.FieldName) + if sortBy.Asc && sortBy.Desc { + cmd := NewAggregateCmd(ctx, args...) + cmd.SetErr(fmt.Errorf("FT.AGGREGATE: ASC and DESC are mutually exclusive")) + return cmd + } + if sortBy.Asc { + sortByOptions = append(sortByOptions, "ASC") + } + if sortBy.Desc { + sortByOptions = append(sortByOptions, "DESC") + } + } + args = append(args, len(sortByOptions)) + args = append(args, sortByOptions...) + } + if options.SortByMax > 0 { + args = append(args, "MAX", options.SortByMax) + } } if options.LimitOffset >= 0 && options.Limit > 0 { args = append(args, "LIMIT", options.LimitOffset, options.Limit) @@ -1409,134 +1689,322 @@ func (c cmdable) FTExplainCli(ctx context.Context, key, path string) error { return fmt.Errorf("FTExplainCli is not implemented") } +// parseFTAttributeFromMap parses an FTAttribute from a RESP3 map format +func parseFTAttributeFromMap(attrMap map[interface{}]interface{}) FTAttribute { + att := FTAttribute{} + for k, v := range attrMap { + key := internal.ToLower(internal.ToString(k)) + switch key { + case "attribute": + att.Attribute = internal.ToString(v) + case "identifier": + att.Identifier = internal.ToString(v) + case "type": + att.Type = internal.ToString(v) + case "weight": + att.Weight = internal.ToFloat(v) + case "phonetic": + att.PhoneticMatcher = internal.ToString(v) + case "algorithm": + att.Algorithm = internal.ToString(v) + case "data_type": + att.DataType = internal.ToString(v) + case "dim": + att.Dim = internal.ToInteger(v) + case "distance_metric": + att.DistanceMetric = internal.ToString(v) + case "m": + att.M = internal.ToInteger(v) + case "ef_construction": + att.EFConstruction = internal.ToInteger(v) + case "flags": + // flags is an array of strings like ["SORTABLE", "NOSTEM"] + if flags, ok := v.([]interface{}); ok { + for _, flag := range flags { + flagStr := internal.ToLower(internal.ToString(flag)) + switch flagStr { + case "nostem": + att.NoStem = true + case "sortable": + att.Sortable = true + case "noindex": + att.NoIndex = true + case "unf": + att.UNF = true + case "case_sensitive": + att.CaseSensitive = true + case "withsuffixtrie": + att.WithSuffixtrie = true + } + } + } + } + } + return att +} + +// getMapStringKey extracts a string value from a map with interface{} keys +func getMapStringKey(m map[interface{}]interface{}, key string) interface{} { + if v, ok := m[key]; ok { + return v + } + return nil +} + +// parseIndexErrorsRESP3 parses Index Errors from RESP3 map format +func parseIndexErrorsRESP3(m map[interface{}]interface{}) IndexErrors { + return IndexErrors{ + IndexingFailures: internal.ToInteger(getMapStringKey(m, "indexing failures")), + LastIndexingError: internal.ToString(getMapStringKey(m, "last indexing error")), + LastIndexingErrorKey: internal.ToString(getMapStringKey(m, "last indexing error key")), + } +} + +// parseCursorStatsRESP3 parses cursor_stats from RESP3 map format +func parseCursorStatsRESP3(m map[interface{}]interface{}) CursorStats { + return CursorStats{ + GlobalIdle: internal.ToInteger(getMapStringKey(m, "global_idle")), + GlobalTotal: internal.ToInteger(getMapStringKey(m, "global_total")), + IndexCapacity: internal.ToInteger(getMapStringKey(m, "index_capacity")), + IndexTotal: internal.ToInteger(getMapStringKey(m, "index_total")), + } +} + +// parseGCStatsRESP3 parses gc_stats from RESP3 map format +func parseGCStatsRESP3(m map[interface{}]interface{}) GCStats { + // Handle average_cycle_time_ms which can be a float64 (including NaN) or string + avgCycleTime := "" + if v := getMapStringKey(m, "average_cycle_time_ms"); v != nil { + switch val := v.(type) { + case string: + // Normalize to lowercase for consistency with RESP2 + avgCycleTime = strings.ToLower(val) + case float64: + avgCycleTime = internal.FormatFloat(val) + } + } + + return GCStats{ + BytesCollected: ftInfoNumInt(getMapStringKey(m, "bytes_collected")), + TotalMsRun: ftInfoNumInt(getMapStringKey(m, "total_ms_run")), + TotalCycles: ftInfoNumInt(getMapStringKey(m, "total_cycles")), + AverageCycleTimeMs: avgCycleTime, + LastRunTimeMs: ftInfoNumInt(getMapStringKey(m, "last_run_time_ms")), + GCNumericTreesMissed: ftInfoNumInt(getMapStringKey(m, "gc_numeric_trees_missed")), + GCBlocksDenied: ftInfoNumInt(getMapStringKey(m, "gc_blocks_denied")), + } +} + +// parseIndexDefinitionRESP3 parses index_definition from RESP3 map format +func parseIndexDefinitionRESP3(m map[interface{}]interface{}) IndexDefinition { + def := IndexDefinition{ + KeyType: internal.ToString(getMapStringKey(m, "key_type")), + DefaultScore: internal.ToFloat(getMapStringKey(m, "default_score")), + } + if prefixes, ok := getMapStringKey(m, "prefixes").([]interface{}); ok { + def.Prefixes = internal.ToStringSlice(prefixes) + } + return def +} + +// parseDialectStatsRESP3 parses dialect_stats from RESP3 map format +func parseDialectStatsRESP3(m map[interface{}]interface{}) map[string]int { + result := make(map[string]int) + for k, v := range m { + if kStr, ok := k.(string); ok { + result[kStr] = internal.ToInteger(v) + } + } + return result +} + +// ftInfoNumString stringifies a value that RediSearch emits via REPLY_KVNUM +// (RedisModule_ReplyWithDouble): a bulk string in RESP2 but a native double +// in RESP3. Used for FTInfoResult fields whose public type is string. +// Special float values (NaN, +Inf, -Inf) are normalized to lowercase to match +// the RESP2 wire format. +func ftInfoNumString(val interface{}) string { + switch v := val.(type) { + case string: + return v + case float64: + return internal.FormatFloat(v) + case float32: + return internal.FormatFloat(float64(v)) + case int64: + return strconv.FormatInt(v, 10) + case int: + return strconv.Itoa(v) + default: + return "" + } +} + +// ftInfoNumInt converts a value that RediSearch emits via REPLY_KVNUM to int. +// In RESP2 the value is a bulk string; in RESP3 it is a native double, even +// for logically-integer fields (counters, byte sizes). This helper exists so +// the internal.ToInteger helper can remain strict about float-to-int coercion +// while still letting the RediSearch parsers read those values correctly. +func ftInfoNumInt(val interface{}) int { + switch v := val.(type) { + case float64: + return int(v) + case float32: + return int(v) + default: + return internal.ToInteger(v) + } +} + func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { var ftInfo FTInfoResult - // Manually parse each field from the map + + // Parse Index Errors - handle both RESP2 (array) and RESP3 (map) formats if indexErrors, ok := data["Index Errors"].([]interface{}); ok { + // RESP2 format: array with key-value pairs ftInfo.IndexErrors = IndexErrors{ IndexingFailures: internal.ToInteger(indexErrors[1]), LastIndexingError: internal.ToString(indexErrors[3]), LastIndexingErrorKey: internal.ToString(indexErrors[5]), } + } else if indexErrors, ok := data["Index Errors"].(map[interface{}]interface{}); ok { + // RESP3 format: map + ftInfo.IndexErrors = parseIndexErrorsRESP3(indexErrors) } if attributes, ok := data["attributes"].([]interface{}); ok { for _, attr := range attributes { - if attrMap, ok := attr.([]interface{}); ok { - att := FTAttribute{} - attrLen := len(attrMap) + att := FTAttribute{} + // Handle RESP2 format: attribute is []interface{} + if attrSlice, ok := attr.([]interface{}); ok { + attrLen := len(attrSlice) for i := 0; i < attrLen; i++ { - if internal.ToLower(internal.ToString(attrMap[i])) == "attribute" && i+1 < attrLen { - att.Attribute = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "attribute" && i+1 < attrLen { + att.Attribute = internal.ToString(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "identifier" && i+1 < attrLen { - att.Identifier = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "identifier" && i+1 < attrLen { + att.Identifier = internal.ToString(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "type" && i+1 < attrLen { - att.Type = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "type" && i+1 < attrLen { + att.Type = internal.ToString(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "weight" && i+1 < attrLen { - att.Weight = internal.ToFloat(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "weight" && i+1 < attrLen { + att.Weight = internal.ToFloat(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "nostem" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "nostem" { att.NoStem = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "sortable" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "sortable" { att.Sortable = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "noindex" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "noindex" { att.NoIndex = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "unf" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "unf" { att.UNF = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "phonetic" && i+1 < attrLen { - att.PhoneticMatcher = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "phonetic" && i+1 < attrLen { + att.PhoneticMatcher = internal.ToString(attrSlice[i+1]) continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "case_sensitive" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "case_sensitive" { att.CaseSensitive = true continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "withsuffixtrie" { + if internal.ToLower(internal.ToString(attrSlice[i])) == "withsuffixtrie" { att.WithSuffixtrie = true continue } // vector specific attributes - if internal.ToLower(internal.ToString(attrMap[i])) == "algorithm" && i+1 < attrLen { - att.Algorithm = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "algorithm" && i+1 < attrLen { + att.Algorithm = internal.ToString(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "data_type" && i+1 < attrLen { - att.DataType = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "data_type" && i+1 < attrLen { + att.DataType = internal.ToString(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "dim" && i+1 < attrLen { - att.Dim = internal.ToInteger(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "dim" && i+1 < attrLen { + att.Dim = internal.ToInteger(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "distance_metric" && i+1 < attrLen { - att.DistanceMetric = internal.ToString(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "distance_metric" && i+1 < attrLen { + att.DistanceMetric = internal.ToString(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "m" && i+1 < attrLen { - att.M = internal.ToInteger(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "m" && i+1 < attrLen { + att.M = internal.ToInteger(attrSlice[i+1]) i++ continue } - if internal.ToLower(internal.ToString(attrMap[i])) == "ef_construction" && i+1 < attrLen { - att.EFConstruction = internal.ToInteger(attrMap[i+1]) + if internal.ToLower(internal.ToString(attrSlice[i])) == "ef_construction" && i+1 < attrLen { + att.EFConstruction = internal.ToInteger(attrSlice[i+1]) i++ continue } - } ftInfo.Attributes = append(ftInfo.Attributes, att) + } else if attrMap, ok := attr.(map[interface{}]interface{}); ok { + // Handle RESP3 format: attribute is map[interface{}]interface{} + att = parseFTAttributeFromMap(attrMap) + ftInfo.Attributes = append(ftInfo.Attributes, att) } } } - ftInfo.BytesPerRecordAvg = internal.ToString(data["bytes_per_record_avg"]) + ftInfo.BytesPerRecordAvg = ftInfoNumString(data["bytes_per_record_avg"]) ftInfo.Cleaning = internal.ToInteger(data["cleaning"]) + // Parse cursor_stats - handle both RESP2 (array) and RESP3 (map) formats if cursorStats, ok := data["cursor_stats"].([]interface{}); ok { + // RESP2 format ftInfo.CursorStats = CursorStats{ GlobalIdle: internal.ToInteger(cursorStats[1]), GlobalTotal: internal.ToInteger(cursorStats[3]), IndexCapacity: internal.ToInteger(cursorStats[5]), IndexTotal: internal.ToInteger(cursorStats[7]), } + } else if cursorStats, ok := data["cursor_stats"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.CursorStats = parseCursorStatsRESP3(cursorStats) } + // Parse dialect_stats - handle both RESP2 (array) and RESP3 (map) formats if dialectStats, ok := data["dialect_stats"].([]interface{}); ok { + // RESP2 format ftInfo.DialectStats = make(map[string]int) for i := 0; i < len(dialectStats); i += 2 { ftInfo.DialectStats[internal.ToString(dialectStats[i])] = internal.ToInteger(dialectStats[i+1]) } + } else if dialectStats, ok := data["dialect_stats"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.DialectStats = parseDialectStatsRESP3(dialectStats) } ftInfo.DocTableSizeMB = internal.ToFloat(data["doc_table_size_mb"]) + // Parse field statistics - handle both RESP2 and RESP3 formats if fieldStats, ok := data["field statistics"].([]interface{}); ok { for _, stat := range fieldStats { if statMap, ok := stat.([]interface{}); ok { + // RESP2 format ftInfo.FieldStatistics = append(ftInfo.FieldStatistics, FieldStatistic{ Identifier: internal.ToString(statMap[1]), Attribute: internal.ToString(statMap[3]), @@ -1546,11 +2014,23 @@ func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { LastIndexingErrorKey: internal.ToString(statMap[5].([]interface{})[5]), }, }) + } else if statMap, ok := stat.(map[interface{}]interface{}); ok { + // RESP3 format + fs := FieldStatistic{ + Identifier: internal.ToString(getMapStringKey(statMap, "identifier")), + Attribute: internal.ToString(getMapStringKey(statMap, "attribute")), + } + if indexErrors, ok := getMapStringKey(statMap, "Index Errors").(map[interface{}]interface{}); ok { + fs.IndexErrors = parseIndexErrorsRESP3(indexErrors) + } + ftInfo.FieldStatistics = append(ftInfo.FieldStatistics, fs) } } } + // Parse gc_stats - handle both RESP2 (array) and RESP3 (map) formats if gcStats, ok := data["gc_stats"].([]interface{}); ok { + // RESP2 format ftInfo.GCStats = GCStats{} for i := 0; i < len(gcStats); i += 2 { if internal.ToLower(internal.ToString(gcStats[i])) == "bytes_collected" { @@ -1582,21 +2062,31 @@ func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { continue } } + } else if gcStats, ok := data["gc_stats"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.GCStats = parseGCStatsRESP3(gcStats) } ftInfo.GeoshapesSzMB = internal.ToFloat(data["geoshapes_sz_mb"]) ftInfo.HashIndexingFailures = internal.ToInteger(data["hash_indexing_failures"]) + // Parse index_definition - handle both RESP2 (array) and RESP3 (map) formats if indexDef, ok := data["index_definition"].([]interface{}); ok { + // RESP2 format ftInfo.IndexDefinition = IndexDefinition{ KeyType: internal.ToString(indexDef[1]), Prefixes: internal.ToStringSlice(indexDef[3]), DefaultScore: internal.ToFloat(indexDef[5]), } + } else if indexDef, ok := data["index_definition"].(map[interface{}]interface{}); ok { + // RESP3 format + ftInfo.IndexDefinition = parseIndexDefinitionRESP3(indexDef) } ftInfo.IndexName = internal.ToString(data["index_name"]) - ftInfo.IndexOptions = internal.ToStringSlice(data["index_options"].([]interface{})) + if indexOptions, ok := data["index_options"].([]interface{}); ok { + ftInfo.IndexOptions = internal.ToStringSlice(indexOptions) + } ftInfo.Indexing = internal.ToInteger(data["indexing"]) ftInfo.InvertedSzMB = internal.ToFloat(data["inverted_sz_mb"]) ftInfo.KeyTableSizeMB = internal.ToFloat(data["key_table_size_mb"]) @@ -1605,16 +2095,16 @@ func parseFTInfo(data map[string]interface{}) (FTInfoResult, error) { ftInfo.NumRecords = internal.ToInteger(data["num_records"]) ftInfo.NumTerms = internal.ToInteger(data["num_terms"]) ftInfo.NumberOfUses = internal.ToInteger(data["number_of_uses"]) - ftInfo.OffsetBitsPerRecordAvg = internal.ToString(data["offset_bits_per_record_avg"]) + ftInfo.OffsetBitsPerRecordAvg = ftInfoNumString(data["offset_bits_per_record_avg"]) ftInfo.OffsetVectorsSzMB = internal.ToFloat(data["offset_vectors_sz_mb"]) - ftInfo.OffsetsPerTermAvg = internal.ToString(data["offsets_per_term_avg"]) + ftInfo.OffsetsPerTermAvg = ftInfoNumString(data["offsets_per_term_avg"]) ftInfo.PercentIndexed = internal.ToFloat(data["percent_indexed"]) - ftInfo.RecordsPerDocAvg = internal.ToString(data["records_per_doc_avg"]) + ftInfo.RecordsPerDocAvg = ftInfoNumString(data["records_per_doc_avg"]) ftInfo.SortableValuesSizeMB = internal.ToFloat(data["sortable_values_size_mb"]) ftInfo.TagOverheadSzMB = internal.ToFloat(data["tag_overhead_sz_mb"]) ftInfo.TextOverheadSzMB = internal.ToFloat(data["text_overhead_sz_mb"]) ftInfo.TotalIndexMemorySzMB = internal.ToFloat(data["total_index_memory_sz_mb"]) - ftInfo.TotalIndexingTime = internal.ToInteger(data["total_indexing_time"]) + ftInfo.TotalIndexingTime = ftInfoNumInt(data["total_indexing_time"]) ftInfo.TotalInvertedIndexBlocks = internal.ToInteger(data["total_inverted_index_blocks"]) ftInfo.VectorIndexSzMB = internal.ToFloat(data["vector_index_sz_mb"]) @@ -1660,6 +2150,37 @@ func (cmd *FTInfoCmd) RawResult() (interface{}, error) { return cmd.rawVal, cmd.err } func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { + readType, err := rd.PeekReplyType() + if err != nil { + return err + } + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + + // Convert map[interface{}]interface{} to map[string]interface{} + rawMap, ok := cmd.rawVal.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + + data := make(map[string]interface{}, len(rawMap)) + for k, v := range rawMap { + if kStr, ok := k.(string); ok { + data[kStr] = v + } + } + + cmd.val, err = parseFTInfo(data) + return err + } + + // RESP2 format - read as map n, err := rd.ReadMapLen() if err != nil { return err @@ -1686,11 +2207,7 @@ func (cmd *FTInfoCmd) readReply(rd *proto.Reader) (err error) { data[k] = v } cmd.val, err = parseFTInfo(data) - if err != nil { - return err - } - - return nil + return err } func (cmd *FTInfoCmd) Clone() Cmder { @@ -1728,26 +2245,19 @@ func (cmd *FTInfoCmd) Clone() Cmder { } // Clone slices and maps if cmd.val.Attributes != nil { - val.Attributes = make([]FTAttribute, len(cmd.val.Attributes)) - copy(val.Attributes, cmd.val.Attributes) + val.Attributes = slices.Clone(cmd.val.Attributes) } if cmd.val.DialectStats != nil { - val.DialectStats = make(map[string]int, len(cmd.val.DialectStats)) - for k, v := range cmd.val.DialectStats { - val.DialectStats[k] = v - } + val.DialectStats = maps.Clone(cmd.val.DialectStats) } if cmd.val.FieldStatistics != nil { - val.FieldStatistics = make([]FieldStatistic, len(cmd.val.FieldStatistics)) - copy(val.FieldStatistics, cmd.val.FieldStatistics) + val.FieldStatistics = slices.Clone(cmd.val.FieldStatistics) } if cmd.val.IndexOptions != nil { - val.IndexOptions = make([]string, len(cmd.val.IndexOptions)) - copy(val.IndexOptions, cmd.val.IndexOptions) + val.IndexOptions = slices.Clone(cmd.val.IndexOptions) } if cmd.val.IndexDefinition.Prefixes != nil { - val.IndexDefinition.Prefixes = make([]string, len(cmd.val.IndexDefinition.Prefixes)) - copy(val.IndexDefinition.Prefixes, cmd.val.IndexDefinition.Prefixes) + val.IndexDefinition.Prefixes = slices.Clone(cmd.val.IndexDefinition.Prefixes) } return &FTInfoCmd{ baseCmd: cmd.cloneBaseCmd(), @@ -1843,15 +2353,117 @@ func (cmd *FTSpellCheckCmd) RawResult() (interface{}, error) { } func (cmd *FTSpellCheckCmd) readReply(rd *proto.Reader) (err error) { - data, err := rd.ReadSlice() + readType, err := rd.PeekReplyType() if err != nil { return err } - cmd.val, err = parseFTSpellCheck(data) + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + + // Parse the raw response into structured result + rawMap, ok := cmd.rawVal.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + + cmd.val, err = parseFTSpellCheckRESP3(rawMap) + return err + } + + // RESP2 format + data, err := rd.ReadSlice() if err != nil { return err } - return nil + cmd.val, err = parseFTSpellCheck(data) + return err +} + +// parseFTSpellCheckRESP3 parses the RESP3 format response from FT.SPELLCHECK. +// RESP3 format: +// +// map{ +// "results": map{ +// "misspelled_term": [ +// map{"suggestion": score}, +// ... +// ], +// ... +// } +// } +func parseFTSpellCheckRESP3(data map[interface{}]interface{}) ([]SpellCheckResult, error) { + results := make([]SpellCheckResult, 0) + + resultsData, ok := data["results"] + if !ok { + return results, nil + } + + resultsMap, ok := resultsData.(map[interface{}]interface{}) + if !ok { + return nil, fmt.Errorf("invalid results format: expected map, got %T", resultsData) + } + + for termKey, suggestionsData := range resultsMap { + term, ok := termKey.(string) + if !ok { + continue + } + + suggestionsArray, ok := suggestionsData.([]interface{}) + if !ok { + continue + } + + suggestions := make([]SpellCheckSuggestion, 0, len(suggestionsArray)) + for _, suggestionData := range suggestionsArray { + suggestionMap, ok := suggestionData.(map[interface{}]interface{}) + if !ok { + continue + } + + for suggKey, scoreVal := range suggestionMap { + suggestion, ok := suggKey.(string) + if !ok { + continue + } + + var score float64 + switch v := scoreVal.(type) { + case float64: + score = v + case int64: + score = float64(v) + case string: + var err error + score, err = strconv.ParseFloat(v, 64) + if err != nil { + continue + } + default: + continue + } + + suggestions = append(suggestions, SpellCheckSuggestion{ + Score: score, + Suggestion: suggestion, + }) + } + } + + results = append(results, SpellCheckResult{ + Term: term, + Suggestions: suggestions, + }) + } + + return results, nil } func parseFTSpellCheck(data []interface{}) ([]SpellCheckResult, error) { @@ -1918,8 +2530,7 @@ func (cmd *FTSpellCheckCmd) Clone() Cmder { Term: result.Term, } if result.Suggestions != nil { - val[i].Suggestions = make([]SpellCheckSuggestion, len(result.Suggestions)) - copy(val[i].Suggestions, result.Suggestions) + val[i].Suggestions = slices.Clone(result.Suggestions) } } } @@ -2058,15 +2669,145 @@ func (cmd *FTSearchCmd) RawResult() (interface{}, error) { } func (cmd *FTSearchCmd) readReply(rd *proto.Reader) (err error) { - data, err := rd.ReadSlice() + readType, err := rd.PeekReplyType() if err != nil { return err } - cmd.val, err = parseFTSearch(data, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + // Parse the raw response into structured result + if mapVal, ok := cmd.rawVal.(map[interface{}]interface{}); ok { + cmd.val, err = parseFTSearchMapRESP3(mapVal) + } else { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + return err + } + + // RESP2 format or error response - use ReadReply to handle errors properly + data, err := rd.ReadReply() if err != nil { return err } - return nil + if dataSlice, ok := data.([]interface{}); ok { + cmd.val, err = parseFTSearch(dataSlice, cmd.options.NoContent, cmd.options.WithScores, cmd.options.WithPayloads, cmd.options.WithSortKeys) + return err + } + return fmt.Errorf("unexpected response type: %T", data) +} + +// parseFTSearchMapRESP3 parses the RESP3 format response from FT.SEARCH. +// It takes a map[interface{}]interface{} which is the raw response from ReadReply(). +// RESP3 format: +// +// %5 +// $10 attributes => *0 +// $13 total_results => :N +// $6 format => $6 STRING +// $7 results => *N (array of maps with id, score, extra_attributes, values) +// $7 warning => *N (array of strings) +func parseFTSearchMapRESP3(data map[interface{}]interface{}) (FTSearchResult, error) { + var result FTSearchResult + result.Docs = make([]Document, 0) + + for k, v := range data { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "total_results": + result.Total = internal.ToInteger(v) + case "results": + if resultsData, ok := v.([]interface{}); ok { + docs, err := parseFTSearchResultsMapRESP3(resultsData) + if err != nil { + return FTSearchResult{}, err + } + result.Docs = docs + } + case "warning": + if warningsData, ok := v.([]interface{}); ok { + result.Warnings = make([]string, 0, len(warningsData)) + for _, w := range warningsData { + if ws, ok := w.(string); ok { + result.Warnings = append(result.Warnings, ws) + } + } + } + // Ignore "attributes", "format", and other fields as per the spec + } + } + + return result, nil +} + +// parseFTSearchResultsMapRESP3 parses the results array from RESP3 FT.SEARCH response. +func parseFTSearchResultsMapRESP3(resultsData []interface{}) ([]Document, error) { + docs := make([]Document, 0, len(resultsData)) + for _, item := range resultsData { + if itemMap, ok := item.(map[interface{}]interface{}); ok { + doc, err := parseFTSearchDocumentMapRESP3(itemMap) + if err != nil { + return nil, err + } + docs = append(docs, doc) + } + } + return docs, nil +} + +// parseFTSearchDocumentMapRESP3 parses a single document from RESP3 FT.SEARCH response. +func parseFTSearchDocumentMapRESP3(itemMap map[interface{}]interface{}) (Document, error) { + doc := Document{ + Fields: make(map[string]string), + } + + for k, v := range itemMap { + key, ok := k.(string) + if !ok { + continue + } + + switch key { + case "id": + if id, ok := v.(string); ok { + doc.ID = id + } + case "score": + if score, ok := v.(float64); ok { + doc.Score = &score + } + case "payload": + if payload, ok := v.(string); ok { + doc.Payload = &payload + } + case "sortkey": + if sortKey, ok := v.(string); ok { + doc.SortKey = &sortKey + } + case "extra_attributes": + if extraAttrs, ok := v.(map[interface{}]interface{}); ok { + for ek, ev := range extraAttrs { + if ekStr, ok := ek.(string); ok { + if evStr, ok := ev.(string); ok { + doc.Fields[ekStr] = evStr + } + } + } + } + // Ignore "values" and other fields as per the spec + } + } + + return doc, nil } func (cmd *FTSearchCmd) Clone() Cmder { @@ -2090,6 +2831,10 @@ func (cmd *FTSearchCmd) Clone() Cmder { } } } + if cmd.val.Warnings != nil { + val.Warnings = make([]string, len(cmd.val.Warnings)) + copy(val.Warnings, cmd.val.Warnings) + } var options *FTSearchOptions if cmd.options != nil { options = &FTSearchOptions{ @@ -2115,34 +2860,25 @@ func (cmd *FTSearchCmd) Clone() Cmder { } // Clone slices and maps if cmd.options.Filters != nil { - options.Filters = make([]FTSearchFilter, len(cmd.options.Filters)) - copy(options.Filters, cmd.options.Filters) + options.Filters = slices.Clone(cmd.options.Filters) } if cmd.options.GeoFilter != nil { - options.GeoFilter = make([]FTSearchGeoFilter, len(cmd.options.GeoFilter)) - copy(options.GeoFilter, cmd.options.GeoFilter) + options.GeoFilter = slices.Clone(cmd.options.GeoFilter) } if cmd.options.InKeys != nil { - options.InKeys = make([]interface{}, len(cmd.options.InKeys)) - copy(options.InKeys, cmd.options.InKeys) + options.InKeys = slices.Clone(cmd.options.InKeys) } if cmd.options.InFields != nil { - options.InFields = make([]interface{}, len(cmd.options.InFields)) - copy(options.InFields, cmd.options.InFields) + options.InFields = slices.Clone(cmd.options.InFields) } if cmd.options.Return != nil { - options.Return = make([]FTSearchReturn, len(cmd.options.Return)) - copy(options.Return, cmd.options.Return) + options.Return = slices.Clone(cmd.options.Return) } if cmd.options.SortBy != nil { - options.SortBy = make([]FTSearchSortBy, len(cmd.options.SortBy)) - copy(options.SortBy, cmd.options.SortBy) + options.SortBy = slices.Clone(cmd.options.SortBy) } if cmd.options.Params != nil { - options.Params = make(map[string]interface{}, len(cmd.options.Params)) - for k, v := range cmd.options.Params { - options.Params[k] = v - } + options.Params = maps.Clone(cmd.options.Params) } } return &FTSearchCmd{ @@ -2368,8 +3104,7 @@ func (cmd *FTHybridCmd) Clone() Cmder { } } if cmd.val.Warnings != nil { - val.Warnings = make([]string, len(cmd.val.Warnings)) - copy(val.Warnings, cmd.val.Warnings) + val.Warnings = slices.Clone(cmd.val.Warnings) } var cursorVal *FTHybridCursorResult @@ -2749,6 +3484,30 @@ func (cmd *FTSynDumpCmd) RawResult() (interface{}, error) { } func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error { + readType, err := rd.PeekReplyType() + if err != nil { + return err + } + + // RESP3 returns a map, RESP2 returns an array + if readType == proto.RespMap { + // Read raw response first for backwards compatibility + cmd.rawVal, err = rd.ReadReply() + if err != nil { + return err + } + + // Parse the raw response into structured result + rawMap, ok := cmd.rawVal.(map[interface{}]interface{}) + if !ok { + return fmt.Errorf("unexpected RESP3 response type: %T", cmd.rawVal) + } + + cmd.val, err = parseFTSynDumpRESP3(rawMap) + return err + } + + // RESP2 format termSynonymPairs, err := rd.ReadSlice() if err != nil { return err @@ -2785,6 +3544,44 @@ func (cmd *FTSynDumpCmd) readReply(rd *proto.Reader) error { return nil } +// parseFTSynDumpRESP3 parses the RESP3 format response from FT.SYNDUMP. +// RESP3 format: +// +// map{ +// "term1": ["synonym_group_id1", ...], +// "term2": ["synonym_group_id2", ...], +// ... +// } +func parseFTSynDumpRESP3(data map[interface{}]interface{}) ([]FTSynDumpResult, error) { + results := make([]FTSynDumpResult, 0, len(data)) + + for termKey, synonymsData := range data { + term, ok := termKey.(string) + if !ok { + continue + } + + synonymsArray, ok := synonymsData.([]interface{}) + if !ok { + continue + } + + synonymList := make([]string, 0, len(synonymsArray)) + for _, syn := range synonymsArray { + if synonym, ok := syn.(string); ok { + synonymList = append(synonymList, synonym) + } + } + + results = append(results, FTSynDumpResult{ + Term: term, + Synonyms: synonymList, + }) + } + + return results, nil +} + func (cmd *FTSynDumpCmd) Clone() Cmder { var val []FTSynDumpResult if cmd.val != nil { @@ -2869,8 +3666,68 @@ func (c cmdable) FTHybrid(ctx context.Context, index string, searchExpr string, return c.FTHybridWithArgs(ctx, index, options) } +func hybridVectorBlob(v Vector) (interface{}, error) { + if v == nil { + return nil, fmt.Errorf("FT.HYBRID: vector data is required") + } + + switch vector := v.(type) { + case *VectorFP32: + return hybridVectorBytes(vector.Val) + case *VectorFloat16: + return hybridVectorBytes(vector.Val) + case *VectorBFloat16: + return hybridVectorBytes(vector.Val) + case *VectorFloat64: + return hybridVectorBytes(vector.Val) + case *VectorInt8: + return hybridVectorBytes(vector.Val) + case *VectorUint8: + return hybridVectorBytes(vector.Val) + case *VectorValues, *VectorRef: + return nil, fmt.Errorf("FT.HYBRID: unsupported vector type %T", v) + default: + values := v.Value() + if len(values) < 2 { + return nil, fmt.Errorf("FT.HYBRID: vector Value must contain a blob at index 1") + } + return values[1], nil + } +} + +func hybridVectorBytes(blob []byte) ([]byte, error) { + if len(blob) == 0 { + return nil, fmt.Errorf("FT.HYBRID: vector blob is required") + } + return blob, nil +} + +// generateVectorParamName returns a parameter name that is not already present +// in params. It is used to pass vector data via the PARAMS mechanism when the +// caller does not provide a VectorParamName, since inline vector blobs are no +// longer supported by Redis. +func generateVectorParamName(params map[string]interface{}) string { + for i := 0; ; i++ { + name := fmt.Sprintf("__vector_param_%d", i) + if _, ok := params[name]; !ok { + return name + } + } +} + // FTHybridWithArgs - Executes a hybrid search with advanced options // FTHybridWithArgs is still experimental, the command behaviour and signature may change +// +// Vector data is always sent through the PARAMS mechanism, because inline vector +// blobs are no longer supported by Redis. For every vector expression whose +// VectorParamName is empty, a unique name is generated (e.g. "__vector_param_0") +// and the corresponding blob is passed via PARAMS. +// +// options.Params is never mutated: the command is built from a local copy that +// combines the caller-provided params with any generated vector parameters. This +// makes it safe to reuse the same *FTHybridOptions across multiple calls. Generated +// names are also reserved against all explicit VectorParamName values, so they never +// collide with explicit names (even those following the "__vector_param_N" pattern). func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FTHybridOptions) *FTHybridCmd { args := []interface{}{"FT.HYBRID", index} @@ -2891,35 +3748,43 @@ func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FT } } + // Vector data is always passed via the PARAMS mechanism (inline vector blobs + // are no longer supported by Redis). When vectors are present, build a local + // copy of the caller-provided params so options.Params is never mutated, and + // pre-reserve any explicit VectorParamName values so generated names never + // collide with them. + params := options.Params + if len(options.VectorExpressions) > 0 { + params = make(map[string]interface{}, len(options.Params)+len(options.VectorExpressions)) + for k, v := range options.Params { + params[k] = v + } + for _, vectorExpr := range options.VectorExpressions { + if vectorExpr.VectorParamName != "" { + params[vectorExpr.VectorParamName] = nil + } + } + } // Add vector expressions for _, vectorExpr := range options.VectorExpressions { args = append(args, "VSIM", "@"+vectorExpr.VectorField) - // For FT.HYBRID, we need to send just the raw vector bytes, not the Value() format - // Value() returns [format, data] but FT.HYBRID expects just the blob - vectorValue := vectorExpr.VectorData.Value() - var vectorBlob interface{} - if len(vectorValue) >= 2 { - // vectorValue is [format, data, ...] - we only want the data part - vectorBlob = vectorValue[1] - } else { - // Fallback for unexpected format - vectorBlob = vectorValue + vectorBlob, err := hybridVectorBlob(vectorExpr.VectorData) + if err != nil { + cmd := newFTHybridCmd(ctx, options, args...) + cmd.SetErr(err) + return cmd } - // If VectorParamName is provided, use PARAMS mechanism (required for Redis 8.6+) - // If not provided, inline the vector blob (works on Redis 8.4/8.5, fails on 8.6+) - if vectorExpr.VectorParamName != "" { - // Use PARAMS mechanism - args = append(args, "$"+vectorExpr.VectorParamName) - if options.Params == nil { - options.Params = make(map[string]interface{}) - } - options.Params[vectorExpr.VectorParamName] = vectorBlob - } else { - // Inline the vector blob (deprecated in Redis 8.6+) - args = append(args, vectorBlob) + // When VectorParamName is not provided, generate a unique name. Generated + // names are tracked only in the local params map, never written back to + // options.Params. + paramName := vectorExpr.VectorParamName + if paramName == "" { + paramName = generateVectorParamName(params) } + args = append(args, "$"+paramName) + params[paramName] = vectorBlob if vectorExpr.Method != "" { args = append(args, vectorExpr.Method) @@ -3030,11 +3895,13 @@ func (c cmdable) FTHybridWithArgs(ctx context.Context, index string, options *FT } // Add PARAMS - if len(options.Params) > 0 { - args = append(args, "PARAMS", len(options.Params)*2) - for key, value := range options.Params { - // Parameter keys should already have '$' prefix from the user - // Don't add it again if it's already there + // Emit from the local params map, which contains the caller-provided params + // plus any generated vector parameter names. options.Params is left untouched. + if len(params) > 0 { + args = append(args, "PARAMS", len(params)*2) + for key, value := range params { + // PARAMS entries are passed without a '$' prefix; they are referenced in + // the query and clauses using "$". args = append(args, key, value) } } diff --git a/vendor/github.com/redis/go-redis/v9/sentinel.go b/vendor/github.com/redis/go-redis/v9/sentinel.go index 24646c14..055b3101 100644 --- a/vendor/github.com/redis/go-redis/v9/sentinel.go +++ b/vendor/github.com/redis/go-redis/v9/sentinel.go @@ -5,8 +5,10 @@ import ( "crypto/tls" "errors" "fmt" + "math/rand" "net" "net/url" + "slices" "strconv" "strings" "sync" @@ -15,7 +17,6 @@ import ( "github.com/redis/go-redis/v9/auth" "github.com/redis/go-redis/v9/internal" "github.com/redis/go-redis/v9/internal/pool" - "github.com/redis/go-redis/v9/internal/rand" "github.com/redis/go-redis/v9/maintnotifications" "github.com/redis/go-redis/v9/push" ) @@ -100,6 +101,10 @@ type FailoverOptions struct { // default: 100 milliseconds DialerRetryTimeout time.Duration + // DialerRetryBackoff controls the delay between dial retry attempts. + // See Options.DialerRetryBackoff for details. + DialerRetryBackoff func(attempt int) time.Duration + ReadTimeout time.Duration WriteTimeout time.Duration ContextTimeoutEnabled bool @@ -155,6 +160,9 @@ type FailoverOptions struct { // Only applies to failover cluster clients. Default is 15 seconds. FailingTimeoutSeconds int + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. UnstableResp3 bool // PushNotificationProcessor is the processor for handling push notifications. @@ -197,6 +205,7 @@ func (opt *FailoverOptions) clientOptions() *Options { DialTimeout: opt.DialTimeout, DialerRetries: opt.DialerRetries, DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, @@ -251,6 +260,7 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options { DialTimeout: opt.DialTimeout, DialerRetries: opt.DialerRetries, DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, @@ -311,6 +321,7 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions { DialTimeout: opt.DialTimeout, DialerRetries: opt.DialerRetries, DialerRetryTimeout: opt.DialerRetryTimeout, + DialerRetryBackoff: opt.DialerRetryBackoff, ReadTimeout: opt.ReadTimeout, WriteTimeout: opt.WriteTimeout, @@ -494,6 +505,7 @@ func setupFailoverConnParams(u *url.URL, o *FailoverOptions) (*FailoverOptions, // NewFailoverClient returns a Redis client that uses Redis Sentinel // for automatic failover. It's safe for concurrent use by multiple // goroutines. +// Passing nil FailoverOptions will cause a panic. func NewFailoverClient(failoverOpt *FailoverOptions) *Client { if failoverOpt == nil { panic("redis: NewFailoverClient nil options") @@ -524,7 +536,8 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client { rdb := &Client{ baseClient: &baseClient{ - opt: opt, + opt: opt, + onClose: &onCloseHooks{}, }, } rdb.init() @@ -548,7 +561,7 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client { panic(fmt.Errorf("redis: failed to create pubsub pool: %w", err)) } - rdb.onClose = rdb.wrappedOnClose(failover.Close) + rdb.onClose.register(onCloseHookIDSentinelFailover, failover.Close) failover.mu.Lock() failover.onFailover = func(ctx context.Context, addr string) { @@ -603,6 +616,8 @@ type SentinelClient struct { *baseClient } +// NewSentinelClient returns a Redis Sentinel client. +// Passing nil Options will cause a panic. func NewSentinelClient(opt *Options) *SentinelClient { if opt == nil { panic("redis: NewSentinelClient nil options") @@ -610,7 +625,8 @@ func NewSentinelClient(opt *Options) *SentinelClient { opt.init() c := &SentinelClient{ baseClient: &baseClient{ - opt: opt, + opt: opt, + onClose: &onCloseHooks{}, }, } @@ -938,6 +954,7 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { errCh <- err return } + once.Do(func() { masterAddr = net.JoinHostPort(addrVal[0], addrVal[1]) // Push working sentinel to the top @@ -946,6 +963,10 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) { internal.Logger.Printf(ctx, "sentinel: selected addr=%s masterAddr=%s", addr, masterAddr) cancel() }) + + if sentinelCli != c.sentinel { + _ = sentinelCli.Close() + } }(i, sentinelAddr) } @@ -995,8 +1016,16 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo c.opt.MasterName, err) } else if len(addrs) > 0 { return addrs, nil + } else if !useDisconnected { + // No error and no replicas β€” valid steady state for master-only setups. + // Preserve the sentinel connection for master discovery and failover + // pub/sub monitoring. Only return early when useDisconnected is false; + // when true, fall through to the discovery loop which passes + // useDisconnected to parseReplicaAddrs (getReplicaAddrs hardcodes false). + return []string{}, nil } else { - // No error and no replicas. + // useDisconnected=true: close sentinel so the discovery loop can call + // setSentinel if it finds disconnected replicas. _ = c.closeSentinel() } } @@ -1029,9 +1058,9 @@ func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected boo } if sentinelReachable { - return []string{}, nil + return nil, nil } - return []string{}, errors.New("redis: all sentinels specified in configuration are unreachable") + return nil, errors.New("redis: all sentinels specified in configuration are unreachable") } func (c *sentinelFailover) getMasterAddr(ctx context.Context, sentinel *SentinelClient) (string, error) { @@ -1128,7 +1157,7 @@ func (c *sentinelFailover) discoverSentinels(ctx context.Context) { } if ip != "" && port != "" { sentinelAddr := net.JoinHostPort(ip, port) - if !contains(c.sentinelAddrs, sentinelAddr) { + if !slices.Contains(c.sentinelAddrs, sentinelAddr) { internal.Logger.Printf(ctx, "sentinel: discovered new sentinel=%q for master=%q", sentinelAddr, c.opt.MasterName) c.sentinelAddrs = append(c.sentinelAddrs, sentinelAddr) @@ -1162,19 +1191,11 @@ func (c *sentinelFailover) listen(pubsub *PubSub) { } } -func contains(slice []string, str string) bool { - for _, s := range slice { - if s == str { - return true - } - } - return false -} - //------------------------------------------------------------------------------ // NewFailoverClusterClient returns a client that supports routing read-only commands // to a replica node. +// Passing nil FailoverOptions will cause a panic. func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient { if failoverOpt == nil { panic("redis: NewFailoverClusterClient nil options") diff --git a/vendor/github.com/redis/go-redis/v9/sortedset_commands.go b/vendor/github.com/redis/go-redis/v9/sortedset_commands.go index 4a6c8f13..b171d7ac 100644 --- a/vendor/github.com/redis/go-redis/v9/sortedset_commands.go +++ b/vendor/github.com/redis/go-redis/v9/sortedset_commands.go @@ -373,6 +373,17 @@ type ZRangeArgs struct { // } // cmd: "ZRange example-key (3 8 ByScore" (3 < score <= 8). // + // When the Rev option is also provided, should be the higher score value and + // should be the lower score value (i.e. reversed order): + // ZRangeArgs{ + // Key: "example-key", + // Start: 8, + // Stop: "(3", + // ByScore: true, + // Rev: true, + // } + // cmd: "ZRange example-key 8 (3 ByScore Rev" (8 >= score > 3, in reverse order). + // // For the ByLex option, it is similar to the deprecated(6.2.0+) ZRangeByLex command. // You can set the and options as follows: // ZRangeArgs{ @@ -383,6 +394,17 @@ type ZRangeArgs struct { // } // cmd: "ZRange example-key [abc (def ByLex" // + // When the Rev option is also provided, should be the lexicographically higher + // value and should be the lower value: + // ZRangeArgs{ + // Key: "example-key", + // Start: "(def", + // Stop: "[abc", + // ByLex: true, + // Rev: true, + // } + // cmd: "ZRange example-key (def [abc ByLex Rev" + // // For normal cases (ByScore==false && ByLex==false), and should be set to the index range (int). // You can read the documentation for more information: https://redis.io/commands/zrange Start interface{} @@ -400,12 +422,7 @@ type ZRangeArgs struct { } func (z ZRangeArgs) appendArgs(args []interface{}) []interface{} { - // For Rev+ByScore/ByLex, we need to adjust the position of and . - if z.Rev && (z.ByScore || z.ByLex) { - args = append(args, z.Key, z.Stop, z.Start) - } else { - args = append(args, z.Key, z.Start, z.Stop) - } + args = append(args, z.Key, z.Start, z.Stop) if z.ByScore { args = append(args, "byscore") @@ -764,7 +781,7 @@ type ZWithKey struct { type ZStore struct { Keys []string Weights []float64 - // Can be SUM, MIN or MAX. + // Can be SUM, MIN, MAX or COUNT. Aggregate string } diff --git a/vendor/github.com/redis/go-redis/v9/stream_commands.go b/vendor/github.com/redis/go-redis/v9/stream_commands.go index 89ae6a1b..71191aec 100644 --- a/vendor/github.com/redis/go-redis/v9/stream_commands.go +++ b/vendor/github.com/redis/go-redis/v9/stream_commands.go @@ -29,11 +29,13 @@ type StreamCmdable interface { XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd + XNack(ctx context.Context, a *XNackArgs) *IntCmd XPending(ctx context.Context, stream, group string) *XPendingCmd XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimCmd + XAutoClaimWithDeleted(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimWithDeletedCmd XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd @@ -359,6 +361,71 @@ func (c cmdable) XAck(ctx context.Context, stream, group string, ids ...string) return cmd } +// XNACK modes. See [XNackArgs.Mode]. +const ( + XNackModeSilent = "SILENT" + XNackModeFail = "FAIL" + XNackModeFatal = "FATAL" +) + +// XNackArgs represents the arguments for the XNACK command (Redis >= 8.8). +// +// XNACK negatively acknowledges one or more messages in a consumer group's +// Pending Entries List (PEL), releasing them back to the group so they can be +// redelivered to another consumer via XREADGROUP. +type XNackArgs struct { + Stream string + Group string + + // Mode controls how the delivery counter is adjusted for each NACKed entry. + // Must be one of [XNackModeSilent], [XNackModeFail], or [XNackModeFatal]: + // - SILENT: the consumer is shutting down or experiencing internal errors + // unrelated to the message. The delivery counter is decremented by 1, + // undoing the increment that happened when the message was delivered. + // - FAIL: the consumer could not process the message (e.g. insufficient + // memory), but another consumer might succeed. The delivery counter is + // left unchanged. + // - FATAL: the message is invalid or suspected malicious. The delivery + // counter is set to MAXINT, which will immediately move the message to + // the Dead Letter Queue (DLQ) if one is configured for the group. + Mode string + + // IDs is the list of message IDs to NACK. All IDs must already be in the + // group's PEL (i.e. previously delivered via XREADGROUP), unless Force is set. + IDs []string + + // RetryCount sets the delivery counter to an explicit value, overriding the + // counter adjustment that would otherwise be applied by Mode. + // Leave nil to let Mode control the counter (the common case). + RetryCount *uint64 + + // Force allows NACKing message IDs that are not yet in the group's PEL, + // creating new unowned NACKed PEL entries for them directly. + // This is analogous to the FORCE flag in XCLAIM. + // Primarily used internally by Redis during AOF rewrite to reconstruct + // NACKed entries, but can also be used to manually inject entries. + Force bool +} + +// XNack executes the XNACK command. See [XNackArgs] for the full argument documentation. +// Requires Redis >= 8.8. +func (c cmdable) XNack(ctx context.Context, a *XNackArgs) *IntCmd { + args := make([]interface{}, 0, 9+len(a.IDs)) + args = append(args, "xnack", a.Stream, a.Group, a.Mode, "ids", len(a.IDs)) + for _, id := range a.IDs { + args = append(args, id) + } + if a.RetryCount != nil { + args = append(args, "retrycount", *a.RetryCount) + } + if a.Force { + args = append(args, "force") + } + cmd := NewIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XPending(ctx context.Context, stream, group string) *XPendingCmd { cmd := NewXPendingCmd(ctx, "xpending", stream, group) _ = c(ctx, cmd) @@ -406,6 +473,13 @@ func (c cmdable) XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimC return cmd } +func (c cmdable) XAutoClaimWithDeleted(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimWithDeletedCmd { + args := xAutoClaimArgs(ctx, a) + cmd := NewXAutoClaimWithDeletedCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + func (c cmdable) XAutoClaimJustID(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimJustIDCmd { args := xAutoClaimArgs(ctx, a) args = append(args, "justid") diff --git a/vendor/github.com/redis/go-redis/v9/string_commands.go b/vendor/github.com/redis/go-redis/v9/string_commands.go index f69d3d05..88a80844 100644 --- a/vendor/github.com/redis/go-redis/v9/string_commands.go +++ b/vendor/github.com/redis/go-redis/v9/string_commands.go @@ -3,6 +3,7 @@ package redis import ( "context" "fmt" + "strings" "time" ) @@ -20,6 +21,8 @@ type StringCmdable interface { Incr(ctx context.Context, key string) *IntCmd IncrBy(ctx context.Context, key string, value int64) *IntCmd IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd + IncrEXInt(ctx context.Context, key string, args IncrEXIntArgs) *IncrEXIntCmd + IncrEXFloat(ctx context.Context, key string, args IncrEXFloatArgs) *IncrEXFloatCmd LCS(ctx context.Context, q *LCSQuery) *LCSCmd MGet(ctx context.Context, keys ...string) *SliceCmd MSet(ctx context.Context, values ...interface{}) *StatusCmd @@ -197,6 +200,129 @@ func (c cmdable) IncrByFloat(ctx context.Context, key string, value float64) *Fl return cmd } +// IncrEXIntArgs are the arguments to IncrEXInt (the BYINT variant of INCREX). +// +// If By is zero and HasBy is false, the server increments by 1. +// HasLBound/HasUBound gate the optional LBOUND/UBOUND clauses so that 0 is a +// valid bound. Expiration is shared with the SET command via ExpirationOption. +type IncrEXIntArgs struct { + By int64 + HasBy bool + + LBound, UBound int64 + HasLBound, HasUBound bool + + // Saturate clamps the result to LBOUND/UBOUND (or LLONG_MAX/MIN when no + // explicit bound is given) when the increment would exceed it. Without + // this flag, out-of-bounds operations are rejected: the key and TTL are + // left unchanged and the reply is [current_value, 0]. + Saturate bool + + // Expiration sets the TTL semantics: EX, PX, EXAT, PXAT, or PERSIST. + Expiration *ExpirationOption + + // ENX applies the expiration only when the key does not already have an + // expiration. Requires Expiration to set one of EX/PX/EXAT/PXAT. + ENX bool +} + +// IncrEXFloatArgs are the arguments to IncrEXFloat (the BYFLOAT variant of +// INCREX). BYFLOAT is always sent β€” even when By is zero β€” to keep the +// operation in float mode on the server side; omitting BYFLOAT would cause +// the server to treat the call as an integer increment by 1. +// HasLBound/HasUBound gate the optional LBOUND/UBOUND clauses so that 0 is +// a valid bound. +type IncrEXFloatArgs struct { + By float64 + + LBound, UBound float64 + HasLBound, HasUBound bool + + // Saturate clamps the result to LBOUND/UBOUND (or Β±LDBL_MAX when no + // explicit bound is given) when the increment would exceed it. Without + // this flag, out-of-bounds operations are rejected: the key and TTL are + // left unchanged and the reply is [current_value, 0]. + Saturate bool + + Expiration *ExpirationOption + + ENX bool +} + +// IncrEXInt Redis `INCREX key [BYINT amount] [LBOUND value] [UBOUND value] +// [SATURATE] [EX seconds | PX ms | EXAT ts | PXAT ts | PERSIST] [ENX]` +// command. +// +// Atomically increments the integer value stored at key, optionally +// constraining the result to a range and applying expiration semantics. +// Returns the new value and the increment that was actually applied. When +// the increment would exceed LBOUND/UBOUND and SATURATE is not set, the key +// and TTL are left unchanged and the reply is [current_value, 0]. +// +// Available since Redis 8.8. +// For more information, see https://redis.io/commands/increx +func (c cmdable) IncrEXInt(ctx context.Context, key string, a IncrEXIntArgs) *IncrEXIntCmd { + args := make([]interface{}, 0, 14) + args = append(args, "increx", key) + if a.HasBy { + args = append(args, "byint", a.By) + } + if a.HasLBound { + args = append(args, "lbound", a.LBound) + } + if a.HasUBound { + args = append(args, "ubound", a.UBound) + } + if a.Saturate { + args = append(args, "saturate") + } + args = appendIncrEXTail(args, a.Expiration, a.ENX) + + cmd := NewIncrEXIntCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// IncrEXFloat Redis `INCREX key [BYFLOAT amount] [LBOUND value] [UBOUND value] +// [SATURATE] [EX seconds | PX ms | EXAT ts | PXAT ts | PERSIST] [ENX]` +// command. +// +// Available since Redis 8.8. +// For more information, see https://redis.io/commands/increx +func (c cmdable) IncrEXFloat(ctx context.Context, key string, a IncrEXFloatArgs) *IncrEXFloatCmd { + args := make([]interface{}, 0, 14) + args = append(args, "increx", key, "byfloat", a.By) + if a.HasLBound { + args = append(args, "lbound", a.LBound) + } + if a.HasUBound { + args = append(args, "ubound", a.UBound) + } + if a.Saturate { + args = append(args, "saturate") + } + args = appendIncrEXTail(args, a.Expiration, a.ENX) + + cmd := NewIncrEXFloatCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +func appendIncrEXTail(args []interface{}, exp *ExpirationOption, enx bool) []interface{} { + if exp != nil { + switch exp.Mode { + case EX, PX, EXAT, PXAT: + args = append(args, strings.ToLower(string(exp.Mode)), exp.Value) + case PERSIST: + args = append(args, "persist") + } + } + if enx { + args = append(args, "enx") + } + return args +} + type SetCondition string const ( @@ -219,6 +345,8 @@ const ( PXAT ExpirationMode = "PXAT" // KEEPTTL keeps the existing TTL KEEPTTL ExpirationMode = "KEEPTTL" + // PERSIST removes the existing TTL. Used by INCREX. + PERSIST ExpirationMode = "PERSIST" ) type ExpirationOption struct { @@ -429,8 +557,6 @@ func (c cmdable) SetEx(ctx context.Context, key string, value interface{}, expir // SetNX sets the value of a key only if the key does not exist. // -// Deprecated: Use Set with NX option instead as of Redis 2.6.12. -// // Zero expiration means the key has no expiration time. // KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0, // otherwise you will receive an error: (error) ERR syntax error. @@ -438,8 +564,7 @@ func (c cmdable) SetNX(ctx context.Context, key string, value interface{}, expir var cmd *BoolCmd switch expiration { case 0: - // Use old `SETNX` to support old Redis versions. - cmd = NewBoolCmd(ctx, "setnx", key, value) + cmd = NewBoolCmd(ctx, "set", key, value, "nx") case KeepTTL: cmd = NewBoolCmd(ctx, "set", key, value, "keepttl", "nx") default: diff --git a/vendor/github.com/redis/go-redis/v9/timeseries_commands.go b/vendor/github.com/redis/go-redis/v9/timeseries_commands.go index 15d80168..db00db80 100644 --- a/vendor/github.com/redis/go-redis/v9/timeseries_commands.go +++ b/vendor/github.com/redis/go-redis/v9/timeseries_commands.go @@ -2,6 +2,9 @@ package redis import ( "context" + "errors" + "fmt" + "strings" "github.com/redis/go-redis/v9/internal/proto" "github.com/redis/go-redis/v9/internal/util" @@ -139,39 +142,88 @@ func (a Aggregator) String() string { } } +var ( + errTSMultiAggregationGroupBy = errors.New("redis: GROUPBY is not allowed when multiple aggregators are specified") + errTSAggregationConflict = errors.New("redis: setting both Aggregator and Aggregators is not allowed; use Aggregators instead because Aggregator is deprecated") +) + +func formatAggregationArgs(aggregator Aggregator, aggregators []Aggregator) (string, int, error) { + if aggregator != Invalid && len(aggregators) > 0 { + return "", 0, errTSAggregationConflict + } + if len(aggregators) == 0 { + if aggregator == Invalid { + return "", 0, nil + } + aggregationArg, err := formatAggregatorArg(aggregator) + if err != nil { + return "", 0, err + } + return aggregationArg, 1, nil + } + + parts := make([]string, len(aggregators)) + for i, agg := range aggregators { + if agg == Invalid { + return "", 0, fmt.Errorf("redis: invalid timeseries aggregator at index %d: Invalid (%d)", i, agg) + } + aggregationArg, err := formatAggregatorArg(agg) + if err != nil { + return "", 0, fmt.Errorf("redis: invalid timeseries aggregator at index %d: %d", i, agg) + } + parts[i] = aggregationArg + } + + return strings.Join(parts, ","), len(parts), nil +} + +func formatAggregatorArg(aggregator Aggregator) (string, error) { + aggregationArg := aggregator.String() + if aggregationArg == "" { + return "", fmt.Errorf("redis: invalid timeseries aggregator: %d", aggregator) + } + return aggregationArg, nil +} + type TSRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool } type TSRevRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool } type TSMRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - WithLabels bool - SelectedLabels []interface{} - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + WithLabels bool + SelectedLabels []interface{} + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool @@ -180,14 +232,16 @@ type TSMRangeOptions struct { } type TSMRevRangeOptions struct { - Latest bool - FilterByTS []int - FilterByValue []int - WithLabels bool - SelectedLabels []interface{} - Count int - Align interface{} + Latest bool + FilterByTS []int + FilterByValue []int + WithLabels bool + SelectedLabels []interface{} + Count int + Align interface{} + // Deprecated: use Aggregators instead. Aggregator Aggregator + Aggregators []Aggregator BucketDuration int BucketTimestamp interface{} Empty bool @@ -483,7 +537,16 @@ func (c cmdable) TSGet(ctx context.Context, key string) *TSTimestampValueCmd { type TSTimestampValue struct { Timestamp int64 Value float64 + Values []float64 +} + +func (tv TSTimestampValue) String() string { + if len(tv.Values) > 0 { + return fmt.Sprintf("{%d %v}", tv.Timestamp, tv.Values) + } + return fmt.Sprintf("{%d %v}", tv.Timestamp, tv.Value) } + type TSTimestampValueCmd struct { baseCmd val TSTimestampValue @@ -541,9 +604,14 @@ func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) { } func (cmd *TSTimestampValueCmd) Clone() Cmder { + val := cmd.val + if cmd.val.Values != nil { + val.Values = make([]float64, len(cmd.val.Values)) + copy(val.Values, cmd.val.Values) + } return &TSTimestampValueCmd{ baseCmd: cmd.cloneBaseCmd(), - val: cmd.val, // TSTimestampValue is a simple struct, can be copied directly + val: val, } } @@ -636,8 +704,14 @@ func (c cmdable) TSRevRangeWithArgs(ctx context.Context, key string, fromTimesta if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, _, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := newTSTimestampValueSliceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -692,8 +766,14 @@ func (c cmdable) TSRangeWithArgs(ctx context.Context, key string, fromTimestamp if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, _, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := newTSTimestampValueSliceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -748,19 +828,38 @@ func (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) { } cmd.val = make([]TSTimestampValue, n) for i := 0; i < n; i++ { - _, _ = rd.ReadArrayLen() - timestamp, err := rd.ReadInt() + itemLen, err := rd.ReadArrayLen() if err != nil { return err } - value, err := rd.ReadString() + + timestamp, err := rd.ReadInt() if err != nil { return err } cmd.val[i].Timestamp = timestamp - cmd.val[i].Value, err = util.ParseStringToFloat(value) - if err != nil { - return err + if itemLen == 2 { + value, err := rd.ReadString() + if err != nil { + return err + } + cmd.val[i].Value, err = util.ParseStringToFloat(value) + if err != nil { + return err + } + continue + } + + cmd.val[i].Values = make([]float64, itemLen-1) + for j := 0; j < itemLen-1; j++ { + value, err := rd.ReadString() + if err != nil { + return err + } + cmd.val[i].Values[j], err = util.ParseStringToFloat(value) + if err != nil { + return err + } } } @@ -772,6 +871,12 @@ func (cmd *TSTimestampValueSliceCmd) Clone() Cmder { if cmd.val != nil { val = make([]TSTimestampValue, len(cmd.val)) copy(val, cmd.val) + for i := range cmd.val { + if cmd.val[i].Values != nil { + val[i].Values = make([]float64, len(cmd.val[i].Values)) + copy(val[i].Values, cmd.val[i].Values) + } + } } return &TSTimestampValueSliceCmd{ baseCmd: cmd.cloneBaseCmd(), @@ -799,6 +904,7 @@ func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp in // For more information - https://redis.io/commands/ts.mrange/ func (c cmdable) TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRangeOptions) *MapStringSliceInterfaceCmd { args := []interface{}{"TS.MRANGE", fromTimestamp, toTimestamp} + multiAggregationCount := 0 if options != nil { if options.Latest { args = append(args, "LATEST") @@ -828,8 +934,15 @@ func (c cmdable) TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTime if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, count, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + multiAggregationCount = count + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -846,6 +959,11 @@ func (c cmdable) TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTime args = append(args, f) } if options != nil { + if multiAggregationCount > 1 && (options.GroupByLabel != nil || options.Reducer != nil) { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(errTSMultiAggregationGroupBy) + return cmd + } if options.GroupByLabel != nil { args = append(args, "GROUPBY", options.GroupByLabel) } @@ -878,6 +996,7 @@ func (c cmdable) TSMRevRange(ctx context.Context, fromTimestamp int, toTimestamp // For more information - https://redis.io/commands/ts.mrevrange/ func (c cmdable) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRevRangeOptions) *MapStringSliceInterfaceCmd { args := []interface{}{"TS.MREVRANGE", fromTimestamp, toTimestamp} + multiAggregationCount := 0 if options != nil { if options.Latest { args = append(args, "LATEST") @@ -907,8 +1026,15 @@ func (c cmdable) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toT if options.Align != nil { args = append(args, "ALIGN", options.Align) } - if options.Aggregator != 0 { - args = append(args, "AGGREGATION", options.Aggregator.String()) + aggregationArg, count, err := formatAggregationArgs(options.Aggregator, options.Aggregators) + if err != nil { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(err) + return cmd + } + multiAggregationCount = count + if aggregationArg != "" { + args = append(args, "AGGREGATION", aggregationArg) } if options.BucketDuration != 0 { args = append(args, options.BucketDuration) @@ -925,6 +1051,11 @@ func (c cmdable) TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toT args = append(args, f) } if options != nil { + if multiAggregationCount > 1 && (options.GroupByLabel != nil || options.Reducer != nil) { + cmd := NewMapStringSliceInterfaceCmd(ctx, args...) + cmd.SetErr(errTSMultiAggregationGroupBy) + return cmd + } if options.GroupByLabel != nil { args = append(args, "GROUPBY", options.GroupByLabel) } diff --git a/vendor/github.com/redis/go-redis/v9/tx.go b/vendor/github.com/redis/go-redis/v9/tx.go index 40bc1d66..b433b402 100644 --- a/vendor/github.com/redis/go-redis/v9/tx.go +++ b/vendor/github.com/redis/go-redis/v9/tx.go @@ -11,7 +11,7 @@ import ( const TxFailedErr = proto.RedisError("redis: transaction failed") // Tx implements Redis transactions as described in -// http://redis.io/topics/transactions. It's NOT safe for concurrent use +// https://redis.io/docs/latest/develop/using-commands/transactions. It's NOT safe for concurrent use // by multiple goroutines, because Exec resets list of watched keys. // // If you don't need WATCH, use Pipeline instead. @@ -24,10 +24,11 @@ type Tx struct { func (c *Client) newTx() *Tx { tx := Tx{ baseClient: baseClient{ - opt: c.opt.clone(), // Clone options to avoid sharing mutable state between transaction and parent client + opt: c.cloneOpt(), // Clone options under optLock to avoid race with initConn connPool: pool.NewStickyConnPool(c.connPool), hooksMixin: c.hooksMixin.clone(), pushProcessor: c.pushProcessor, // Copy push processor from parent client + onClose: &onCloseHooks{}, }, } tx.init() diff --git a/vendor/github.com/redis/go-redis/v9/universal.go b/vendor/github.com/redis/go-redis/v9/universal.go index 2531cb59..b623460c 100644 --- a/vendor/github.com/redis/go-redis/v9/universal.go +++ b/vendor/github.com/redis/go-redis/v9/universal.go @@ -137,6 +137,9 @@ type UniversalOptions struct { // Only applies to cluster clients. Default is 15 seconds. FailingTimeoutSeconds int + // Deprecated: All RediSearch commands now have stable RESP3 parsing and this + // flag is a no-op. It is kept for backwards compatibility and will be removed + // in a future release. UnstableResp3 bool // PushNotificationProcessor is the processor for handling push notifications. @@ -372,6 +375,8 @@ var ( // 3. If the number of Addrs is two or more, or IsClusterMode option is specified, // a ClusterClient is returned. // 4. Otherwise, a single-node Client is returned. +// +// Passing nil UniversalOptions will cause a panic. func NewUniversalClient(opts *UniversalOptions) UniversalClient { if opts == nil { panic("redis: NewUniversalClient nil options") diff --git a/vendor/github.com/redis/go-redis/v9/vectorset_commands.go b/vendor/github.com/redis/go-redis/v9/vectorset_commands.go index 8f99de07..a88bb1cd 100644 --- a/vendor/github.com/redis/go-redis/v9/vectorset_commands.go +++ b/vendor/github.com/redis/go-redis/v9/vectorset_commands.go @@ -15,8 +15,8 @@ type VectorSetCmdable interface { VEmb(ctx context.Context, key, element string, raw bool) *SliceCmd VGetAttr(ctx context.Context, key, element string) *StringCmd VInfo(ctx context.Context, key string) *MapStringInterfaceCmd - VLinks(ctx context.Context, key, element string) *StringSliceCmd - VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd + VLinks(ctx context.Context, key, element string) *StringSliceSliceCmd + VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceSliceCmd VRandMember(ctx context.Context, key string) *StringCmd VRandMemberCount(ctx context.Context, key string, count int) *StringSliceCmd VRem(ctx context.Context, key, element string) *BoolCmd @@ -26,7 +26,10 @@ type VectorSetCmdable interface { VSimWithScores(ctx context.Context, key string, val Vector) *VectorScoreSliceCmd VSimWithArgs(ctx context.Context, key string, val Vector, args *VSimArgs) *StringSliceCmd VSimWithArgsWithScores(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreSliceCmd + VSimWithArgsWithAttribs(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorAttribSliceCmd + VSimWithArgsWithScoresWithAttribs(ctx context.Context, key string, val Vector, args *VSimArgs) *VectorScoreAttribSliceCmd VRange(ctx context.Context, key, start, end string, count int64) *StringSliceCmd + VIsMember(ctx context.Context, key, element string) *BoolCmd } type Vector interface { @@ -36,6 +39,11 @@ type Vector interface { const ( vectorFormatFP32 string = "FP32" vectorFormatValues string = "Values" + vectorFormatF16 string = "FLOAT16" + vectorFormatBF16 string = "BFLOAT16" + vectorFormatF64 string = "FLOAT64" + vectorFormatI8 string = "INT8" + vectorFormatU8 string = "UINT8" ) type VectorFP32 struct { @@ -48,6 +56,66 @@ func (v *VectorFP32) Value() []any { var _ Vector = (*VectorFP32)(nil) +// VectorFloat16 represents a FLOAT16-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorFloat16 struct { + Val []byte +} + +func (v *VectorFloat16) Value() []any { + return []any{vectorFormatF16, v.Val} +} + +var _ Vector = (*VectorFloat16)(nil) + +// VectorBFloat16 represents a BFLOAT16-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorBFloat16 struct { + Val []byte +} + +func (v *VectorBFloat16) Value() []any { + return []any{vectorFormatBF16, v.Val} +} + +var _ Vector = (*VectorBFloat16)(nil) + +// VectorFloat64 represents a FLOAT64-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorFloat64 struct { + Val []byte +} + +func (v *VectorFloat64) Value() []any { + return []any{vectorFormatF64, v.Val} +} + +var _ Vector = (*VectorFloat64)(nil) + +// VectorInt8 represents an INT8-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorInt8 struct { + Val []byte +} + +func (v *VectorInt8) Value() []any { + return []any{vectorFormatI8, v.Val} +} + +var _ Vector = (*VectorInt8)(nil) + +// VectorUint8 represents a UINT8-encoded vector blob. +// note: intended for search/index query commands such as FT.HYBRID. +type VectorUint8 struct { + Val []byte +} + +func (v *VectorUint8) Value() []any { + return []any{vectorFormatU8, v.Val} +} + +var _ Vector = (*VectorUint8)(nil) + type VectorValues struct { Val []float64 } @@ -79,6 +147,17 @@ type VectorScore struct { Score float64 } +type VectorAttrib struct { + Name string + Attribs *string +} + +type VectorScoreAttrib struct { + Name string + Score float64 + Attribs *string +} + // `VADD key (FP32 | VALUES num) vector element` // note: the API is experimental and may be subject to change. func (c cmdable) VAdd(ctx context.Context, key, element string, val Vector) *BoolCmd { @@ -193,16 +272,16 @@ func (c cmdable) VInfo(ctx context.Context, key string) *MapStringInterfaceCmd { // `VLINKS key element` // note: the API is experimental and may be subject to change. -func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceCmd { - cmd := NewStringSliceCmd(ctx, "vlinks", key, element) +func (c cmdable) VLinks(ctx context.Context, key, element string) *StringSliceSliceCmd { + cmd := NewStringSliceSliceCmd(ctx, "vlinks", key, element) _ = c(ctx, cmd) return cmd } // `VLINKS key element WITHSCORES` // note: the API is experimental and may be subject to change. -func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceCmd { - cmd := NewVectorInfoSliceCmd(ctx, "vlinks", key, element, "withscores") +func (c cmdable) VLinksWithScores(ctx context.Context, key, element string) *VectorScoreSliceSliceCmd { + cmd := NewVectorScoreSliceSliceCmd(ctx, "vlinks", key, element, "withscores") _ = c(ctx, cmd) return cmd } @@ -311,7 +390,7 @@ func (v VSimArgs) appendArgs(args []any) []any { args = append(args, "nothread") } if v.Epsilon > 0 { - args = append(args, "Epsilon", v.Epsilon) + args = append(args, "epsilon", v.Epsilon) } return args } @@ -347,6 +426,40 @@ func (c cmdable) VSimWithArgsWithScores(ctx context.Context, key string, val Vec return cmd } +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHATTRIBS] [COUNT num] [EPSILON delta] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +// WITHATTRIBS is only available in Redis v8.2.0+ +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithArgsWithAttribs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorAttribSliceCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = append(args, "withattribs") + args = simArgs.appendArgs(args) + cmd := NewVectorAttribSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + +// `VSIM key (ELE | FP32 | VALUES num) (vector | element) [WITHSCORES] [WITHATTRIBS] [COUNT num] [EPSILON delta] +// [EF search-exploration-factor] [FILTER expression] [FILTER-EF max-filtering-effort] [TRUTH] [NOTHREAD]` +// WITHATTRIBS is only available in Redis v8.2.0+ +// note: the API is experimental and may be subject to change. +func (c cmdable) VSimWithArgsWithScoresWithAttribs(ctx context.Context, key string, val Vector, simArgs *VSimArgs) *VectorScoreAttribSliceCmd { + if simArgs == nil { + simArgs = &VSimArgs{} + } + args := []any{"vsim", key} + args = append(args, val.Value()...) + args = append(args, "withscores", "withattribs") + args = simArgs.appendArgs(args) + cmd := NewVectorScoreAttribSliceCmd(ctx, args...) + _ = c(ctx, cmd) + return cmd +} + // `VRANGE key start end count` // a negative count means to return all the elements in the vector set. // note: the API is experimental and may be subject to change. @@ -356,3 +469,12 @@ func (c cmdable) VRange(ctx context.Context, key, start, end string, count int64 _ = c(ctx, cmd) return cmd } + +// `VISMEMBER key element` +// Check if an element exists in a vector set. +// note: the API is experimental and may be subject to change. +func (c cmdable) VIsMember(ctx context.Context, key, element string) *BoolCmd { + cmd := NewBoolCmd(ctx, "vismember", key, element) + _ = c(ctx, cmd) + return cmd +} diff --git a/vendor/github.com/redis/go-redis/v9/version.go b/vendor/github.com/redis/go-redis/v9/version.go index 49f001e5..d5938117 100644 --- a/vendor/github.com/redis/go-redis/v9/version.go +++ b/vendor/github.com/redis/go-redis/v9/version.go @@ -2,5 +2,5 @@ package redis // Version is the current release version. func Version() string { - return "9.18.0" + return "9.20.1" } diff --git a/vendor/go.opentelemetry.io/otel/.golangci.yml b/vendor/go.opentelemetry.io/otel/.golangci.yml index d12c8920..db1f5510 100644 --- a/vendor/go.opentelemetry.io/otel/.golangci.yml +++ b/vendor/go.opentelemetry.io/otel/.golangci.yml @@ -17,6 +17,7 @@ linters: - ineffassign - misspell - modernize + - noctx - perfsprint - revive - staticcheck @@ -88,6 +89,16 @@ linters: deny: - pkg: go.opentelemetry.io/otel/exporters/otlp/otlptrace/internal desc: Do not use cross-module internal packages. + semconv: + list-mode: lax + files: + - "!**/semconv/**" + - "!**/exporters/zipkin/**" + deny: + - pkg: go.opentelemetry.io/otel/semconv + desc: "Use go.opentelemetry.io/otel/semconv/v1.40.0 instead. If a newer semconv version has been released, update the depguard rule." + allow: + - go.opentelemetry.io/otel/semconv/v1.40.0 gocritic: disabled-checks: - appendAssign diff --git a/vendor/go.opentelemetry.io/otel/CHANGELOG.md b/vendor/go.opentelemetry.io/otel/CHANGELOG.md index f0595b5e..20edda44 100644 --- a/vendor/go.opentelemetry.io/otel/CHANGELOG.md +++ b/vendor/go.opentelemetry.io/otel/CHANGELOG.md @@ -8,6 +8,75 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] + + + +## [1.43.0/0.65.0/0.19.0] 2026-04-02 + +### Added + +- Add `IsRandom` and `WithRandom` on `TraceFlags`, and `IsRandom` on `SpanContext` in `go.opentelemetry.io/otel/trace` for [W3C Trace Context Level 2 Random Trace ID Flag](https://www.w3.org/TR/trace-context-2/#random-trace-id-flag) support. (#8012) +- Add service detection with `WithService` in `go.opentelemetry.io/otel/sdk/resource`. (#7642) +- Add `DefaultWithContext` and `EnvironmentWithContext` in `go.opentelemetry.io/otel/sdk/resource` to support plumbing `context.Context` through default and environment detectors. (#8051) +- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#8038) +- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc`. (#8038) +- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#8038) +- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#8038) +- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#8038) +- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp`. (#8038) +- Support attributes with empty value (`attribute.EMPTY`) in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8038) +- Add support for per-series start time tracking for cumulative metrics in `go.opentelemetry.io/otel/sdk/metric`. + Set `OTEL_GO_X_PER_SERIES_START_TIMESTAMPS=true` to enable. (#8060) +- Add `WithCardinalityLimitSelector` for metric reader for configuring cardinality limits specific to the instrument kind. (#7855) + +### Changed + +- Introduce the `EMPTY` Type in `go.opentelemetry.io/otel/attribute` to reflect that an empty value is now a valid value, with `INVALID` remaining as a deprecated alias of `EMPTY`. (#8038) +- Improve slice handling in `go.opentelemetry.io/otel/attribute` to optimize short slice values with fixed-size fast paths. (#8039) +- Improve performance of span metric recording in `go.opentelemetry.io/otel/sdk/trace` by returning early if self-observability is not enabled. (#8067) +- Improve formatting of metric data diffs in `go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest`. (#8073) + +### Deprecated + +- Deprecate `INVALID` in `go.opentelemetry.io/otel/attribute`. Use `EMPTY` instead. (#8038) + +### Fixed + +- Return spec-compliant `TraceIdRatioBased` description. This is a breaking behavioral change, but it is necessary to + make the implementation [spec-compliant](https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased). (#8027) +- Fix a race condition in `go.opentelemetry.io/otel/sdk/metric` where the lastvalue aggregation could collect the value 0 even when no zero-value measurements were recorded. (#8056) +- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp` to mitigate excessive memory usage caused by a misconfigured or malicious server. + Responses exceeding the limit are treated as non-retryable errors. (#8108) +- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` to mitigate excessive memory usage caused by a misconfigured or malicious server. + Responses exceeding the limit are treated as non-retryable errors. (#8108) +- Limit HTTP response body to 4 MiB in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to mitigate excessive memory usage caused by a misconfigured or malicious server. + Responses exceeding the limit are treated as non-retryable errors. (#8108) +- `WithHostID` detector in `go.opentelemetry.io/otel/sdk/resource` to use full path for `kenv` command on BSD. (#8113) +- Fix missing `request.GetBody` in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp` to correctly handle HTTP2 GOAWAY frame. (#8096) + +## [1.42.0/0.64.0/0.18.0/0.0.16] 2026-03-06 + +### Added + +- Add `go.opentelemetry.io/otel/semconv/v1.40.0` package. + The package contains semantic conventions from the `v1.40.0` version of the OpenTelemetry Semantic Conventions. + See the [migration documentation](./semconv/v1.40.0/MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/v1.39.0`. (#7985) +- Add `Err` and `SetErr` on `Record` in `go.opentelemetry.io/otel/log` to attach an error and set record exception attributes in `go.opentelemetry.io/otel/log/sdk`. (#7924) + +### Changed + +- `TracerProvider.ForceFlush` in `go.opentelemetry.io/otel/sdk/trace` joins errors together and continues iteration through SpanProcessors as opposed to returning the first encountered error without attempting exports on subsequent SpanProcessors. (#7856) + +### Fixed + +- Fix missing `request.GetBody` in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp` to correctly handle HTTP2 GOAWAY frame. (#7931) +- Fix semconv v1.39.0 generated metric helpers skipping required attributes when extra attributes were empty. (#7964) +- Preserve W3C TraceFlags bitmask (including the random Trace ID flag) during trace context extraction and injection in `go.opentelemetry.io/otel/propagation`. (#7834) + +### Removed + +- Drop support for [Go 1.24]. (#7984) + ## [1.41.0/0.63.0/0.17.0/0.0.15] 2026-03-02 This release is the last to support [Go 1.24]. @@ -26,9 +95,6 @@ The next release will require at least [Go 1.25]. - Return an error when the endpoint is configured as insecure and with TLS configuration in `go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp`. (#7914) - Return an error when the endpoint is configured as insecure and with TLS configuration in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp`. (#7914) - - - ## [1.40.0/0.62.0/0.16.0] 2026-02-02 ### Added @@ -3553,7 +3619,9 @@ It contains api and sdk for trace and meter. - CircleCI build CI manifest files. - CODEOWNERS file to track owners of this project. -[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...HEAD +[Unreleased]: https://github.com/open-telemetry/opentelemetry-go/compare/v1.43.0...HEAD +[1.43.0/0.65.0/0.19.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.43.0 +[1.42.0/0.64.0/0.18.0/0.0.16]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.42.0 [1.41.0/0.63.0/0.17.0/0.0.15]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.41.0 [1.40.0/0.62.0/0.16.0]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.40.0 [1.39.0/0.61.0/0.15.0/0.0.14]: https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.39.0 diff --git a/vendor/go.opentelemetry.io/otel/CONTRIBUTING.md b/vendor/go.opentelemetry.io/otel/CONTRIBUTING.md index 38dede93..12de3607 100644 --- a/vendor/go.opentelemetry.io/otel/CONTRIBUTING.md +++ b/vendor/go.opentelemetry.io/otel/CONTRIBUTING.md @@ -746,8 +746,8 @@ Encapsulate setup in constructor functions, ensuring clear ownership and scope: import ( "errors" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" - "go.opentelemetry.io/otel/semconv/v1.39.0/otelconv" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" + "go.opentelemetry.io/otel/semconv/v1.40.0/otelconv" ) type SDKComponent struct { @@ -1039,7 +1039,7 @@ func (e *Exporter) ExportSpans(ctx context.Context, spans []trace.ReadOnlySpan) All observability metrics should follow the [OpenTelemetry Semantic Conventions for SDK metrics](https://github.com/open-telemetry/semantic-conventions/blob/1cf2476ae5e518225a766990a28a6d5602bd5a30/docs/otel/sdk-metrics.md). -Use the metric semantic conventions convenience package [otelconv](./semconv/v1.39.0/otelconv/metric.go). +Use the metric semantic conventions convenience package [otelconv](./semconv/v1.40.0/otelconv/metric.go). ##### Component Identification diff --git a/vendor/go.opentelemetry.io/otel/Makefile b/vendor/go.opentelemetry.io/otel/Makefile index fc4befb2..42466f2d 100644 --- a/vendor/go.opentelemetry.io/otel/Makefile +++ b/vendor/go.opentelemetry.io/otel/Makefile @@ -38,10 +38,14 @@ CROSSLINK = $(TOOLS)/crosslink $(TOOLS)/crosslink: PACKAGE=go.opentelemetry.io/build-tools/crosslink SEMCONVKIT = $(TOOLS)/semconvkit +SEMCONVKIT_FILES := $(sort $(shell find $(TOOLS_MOD_DIR)/semconvkit -type f)) $(TOOLS)/semconvkit: PACKAGE=go.opentelemetry.io/otel/$(TOOLS_MOD_DIR)/semconvkit +$(TOOLS)/semconvkit: $(SEMCONVKIT_FILES) VERIFYREADMES = $(TOOLS)/verifyreadmes +VERIFYREADMES_FILES := $(sort $(shell find $(TOOLS_MOD_DIR)/verifyreadmes -type f)) $(TOOLS)/verifyreadmes: PACKAGE=go.opentelemetry.io/otel/$(TOOLS_MOD_DIR)/verifyreadmes +$(TOOLS)/verifyreadmes: $(VERIFYREADMES_FILES) GOLANGCI_LINT = $(TOOLS)/golangci-lint $(TOOLS)/golangci-lint: PACKAGE=github.com/golangci/golangci-lint/v2/cmd/golangci-lint @@ -185,11 +189,10 @@ test-coverage: $(GOCOVMERGE) .PHONY: benchmark benchmark: $(OTEL_GO_MOD_DIRS:%=benchmark/%) benchmark/%: - @echo "$(GO) test -run=xxxxxMatchNothingxxxxx -bench=. $*..." \ - && cd $* \ - && $(GO) list ./... \ - | grep -v third_party \ - | xargs $(GO) test -run=xxxxxMatchNothingxxxxx -bench=. + cd $* && $(GO) test -run='^$$' -bench=. $(ARGS) ./... + +print-sharded-benchmarks: + @echo $(OTEL_GO_MOD_DIRS) | jq -cR 'split(" ")' .PHONY: golangci-lint golangci-lint-fix golangci-lint-fix: ARGS=--fix diff --git a/vendor/go.opentelemetry.io/otel/README.md b/vendor/go.opentelemetry.io/otel/README.md index 6b1e170c..16a72004 100644 --- a/vendor/go.opentelemetry.io/otel/README.md +++ b/vendor/go.opentelemetry.io/otel/README.md @@ -55,25 +55,18 @@ Currently, this project supports the following environments. |----------|------------|--------------| | Ubuntu | 1.26 | amd64 | | Ubuntu | 1.25 | amd64 | -| Ubuntu | 1.24 | amd64 | | Ubuntu | 1.26 | 386 | | Ubuntu | 1.25 | 386 | -| Ubuntu | 1.24 | 386 | | Ubuntu | 1.26 | arm64 | | Ubuntu | 1.25 | arm64 | -| Ubuntu | 1.24 | arm64 | | macOS | 1.26 | amd64 | | macOS | 1.25 | amd64 | -| macOS | 1.24 | amd64 | | macOS | 1.26 | arm64 | | macOS | 1.25 | arm64 | -| macOS | 1.24 | arm64 | | Windows | 1.26 | amd64 | | Windows | 1.25 | amd64 | -| Windows | 1.24 | amd64 | | Windows | 1.26 | 386 | | Windows | 1.25 | 386 | -| Windows | 1.24 | 386 | While this project should work for other systems, no compatibility guarantees are made for those systems currently. diff --git a/vendor/go.opentelemetry.io/otel/RELEASING.md b/vendor/go.opentelemetry.io/otel/RELEASING.md index 861756fd..6aff7548 100644 --- a/vendor/go.opentelemetry.io/otel/RELEASING.md +++ b/vendor/go.opentelemetry.io/otel/RELEASING.md @@ -4,7 +4,9 @@ Create a `Version Release` issue to track the release process. -## Semantic Convention Generation +## Semantic Convention Upgrade + +### Semantic Convention Generation New versions of the [OpenTelemetry Semantic Conventions] mean new versions of the `semconv` package need to be generated. The `semconv-generate` make target is used for this. @@ -22,6 +24,43 @@ make semconv-generate # Uses the exported TAG. This should create a new sub-package of [`semconv`](./semconv). Ensure things look correct before submitting a pull request to include the addition. +The `CHANGELOG.md` should also be updated to reflect the new changes: + +```md +- The `go.opentelemetry.io/otel/semconv/` package. The package contains semantic conventions from the `` version of the OpenTelemetry Semantic Conventions. See the [migration documentation](./semconv//MIGRATION.md) for information on how to upgrade from `go.opentelemetry.io/otel/semconv/`. (#PR_NUMBER) +``` + +> **Tip:** Change to the release and prior version to match the changes + +### Update semconv imports + +Once the new semconv module has been generated, update all semconv imports throughout the codebase to reference the new version: + +```go +// Before +semconv "go.opentelemetry.io/otel/semconv/v1.37.0" +"go.opentelemetry.io/otel/semconv/v1.37.0/otelconv" + + +// After +semconv "go.opentelemetry.io/otel/semconv/v1.39.0" +"go.opentelemetry.io/otel/semconv/v1.39.0/otelconv" +``` + +Once complete, run `make` to check for any compilation or test failures. + +#### Handling attribute changes + +Some semconv releases might add new attributes or impact attributes that are currently being used. Changes could stem from a simple renaming, to more complex changes like merging attributes and property values being changed. + +One should update the code to the new attributes that supersede the impacted ones, hence sticking to the semantic conventions. However, legacy attributes might still be emitted in accordance to the `OTEL_SEMCONV_STABILITY_OPT_IN` environment variable. + +For an example on how such migration might have to be tracked and performed, see issue [#7806](https://github.com/open-telemetry/opentelemetry-go/issues/7806). + +### Go contrib linter update + +Update [.golangci.yml](https://github.com/open-telemetry/opentelemetry-go-contrib/blob/main/.golangci.yml) in [opentelemetry-go-contrib](https://github.com/open-telemetry/opentelemetry-go-contrib/) to mandate the new semconv version. + ## Breaking changes validation You can run `make gorelease` which runs [gorelease](https://pkg.go.dev/golang.org/x/exp/cmd/gorelease) to ensure that there are no unwanted changes made in the public API. diff --git a/vendor/go.opentelemetry.io/otel/attribute/encoder.go b/vendor/go.opentelemetry.io/otel/attribute/encoder.go index 6cc1a165..771dd69c 100644 --- a/vendor/go.opentelemetry.io/otel/attribute/encoder.go +++ b/vendor/go.opentelemetry.io/otel/attribute/encoder.go @@ -53,7 +53,7 @@ var ( _ Encoder = &defaultAttrEncoder{} // encoderIDCounter is for generating IDs for other attribute encoders. - encoderIDCounter uint64 + encoderIDCounter atomic.Uint64 defaultEncoderOnce sync.Once defaultEncoderID = NewEncoderID() @@ -64,7 +64,7 @@ var ( // once per each type of attribute encoder. Preferably in init() or in var // definition. func NewEncoderID() EncoderID { - return EncoderID{value: atomic.AddUint64(&encoderIDCounter, 1)} + return EncoderID{value: encoderIDCounter.Add(1)} } // DefaultEncoder returns an attribute encoder that encodes attributes in such diff --git a/vendor/go.opentelemetry.io/otel/attribute/hash.go b/vendor/go.opentelemetry.io/otel/attribute/hash.go index 6aa69aea..b09caaa6 100644 --- a/vendor/go.opentelemetry.io/otel/attribute/hash.go +++ b/vendor/go.opentelemetry.io/otel/attribute/hash.go @@ -27,6 +27,7 @@ const ( int64SliceID uint64 = 3762322556277578591 // "_[]int64" (little endian) float64SliceID uint64 = 7308324551835016539 // "[]double" (little endian) stringSliceID uint64 = 7453010373645655387 // "[]string" (little endian) + emptyID uint64 = 7305809155345288421 // "__empty_" (little endian) ) // hashKVs returns a new xxHash64 hash of kvs. @@ -80,7 +81,8 @@ func hashKV(h xxhash.Hash, kv KeyValue) xxhash.Hash { for i := 0; i < rv.Len(); i++ { h = h.String(rv.Index(i).String()) } - case INVALID: + case EMPTY: + h = h.Uint64(emptyID) default: // Logging is an alternative, but using the internal logger here // causes an import cycle so it is not done. diff --git a/vendor/go.opentelemetry.io/otel/attribute/internal/attribute.go b/vendor/go.opentelemetry.io/otel/attribute/internal/attribute.go index 7f5eae87..d9f51fa2 100644 --- a/vendor/go.opentelemetry.io/otel/attribute/internal/attribute.go +++ b/vendor/go.opentelemetry.io/otel/attribute/internal/attribute.go @@ -11,80 +11,63 @@ import ( "reflect" ) -// BoolSliceValue converts a bool slice into an array with same elements as slice. -func BoolSliceValue(v []bool) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[bool]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() +// sliceElem is the exact set of element types stored in attribute slice values. +// Using a closed set prevents accidental instantiations for unsupported types. +type sliceElem interface { + bool | int64 | float64 | string } -// Int64SliceValue converts an int64 slice into an array with same elements as slice. -func Int64SliceValue(v []int64) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() -} - -// Float64SliceValue converts a float64 slice into an array with same elements as slice. -func Float64SliceValue(v []float64) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[float64]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() -} +// SliceValue converts a slice into an array with the same elements. +func SliceValue[T sliceElem](v []T) any { + // Keep only the common tiny-slice cases out of reflection. Extending this + // much further increases code size for diminishing benefit while larger + // slices still need the generic reflective path to preserve comparability. + // This matches the short lengths that show up most often in local + // benchmarks and semantic convention examples while leaving larger, less + // predictable slices on the generic reflective path. + switch len(v) { + case 0: + return [0]T{} + case 1: + return [1]T{v[0]} + case 2: + return [2]T{v[0], v[1]} + case 3: + return [3]T{v[0], v[1], v[2]} + } -// StringSliceValue converts a string slice into an array with same elements as slice. -func StringSliceValue(v []string) any { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[string]())).Elem() - reflect.Copy(cp, reflect.ValueOf(v)) - return cp.Interface() + return sliceValueReflect(v) } -// AsBoolSlice converts a bool array into a slice into with same elements as array. -func AsBoolSlice(v any) []bool { - rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { - return nil +// AsSlice converts an array into a slice with the same elements. +func AsSlice[T sliceElem](v any) []T { + // Mirror the small fixed-array fast path used by SliceValue. + switch a := v.(type) { + case [0]T: + return []T{} + case [1]T: + return []T{a[0]} + case [2]T: + return []T{a[0], a[1]} + case [3]T: + return []T{a[0], a[1], a[2]} } - cpy := make([]bool, rv.Len()) - if len(cpy) > 0 { - _ = reflect.Copy(reflect.ValueOf(cpy), rv) - } - return cpy -} -// AsInt64Slice converts an int64 array into a slice into with same elements as array. -func AsInt64Slice(v any) []int64 { - rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { - return nil - } - cpy := make([]int64, rv.Len()) - if len(cpy) > 0 { - _ = reflect.Copy(reflect.ValueOf(cpy), rv) - } - return cpy + return asSliceReflect[T](v) } -// AsFloat64Slice converts a float64 array into a slice into with same elements as array. -func AsFloat64Slice(v any) []float64 { - rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { - return nil - } - cpy := make([]float64, rv.Len()) - if len(cpy) > 0 { - _ = reflect.Copy(reflect.ValueOf(cpy), rv) - } - return cpy +func sliceValueReflect[T sliceElem](v []T) any { + cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[T]())).Elem() + reflect.Copy(cp, reflect.ValueOf(v)) + return cp.Interface() } -// AsStringSlice converts a string array into a slice into with same elements as array. -func AsStringSlice(v any) []string { +func asSliceReflect[T sliceElem](v any) []T { rv := reflect.ValueOf(v) - if rv.Type().Kind() != reflect.Array { + if !rv.IsValid() || rv.Kind() != reflect.Array || rv.Type().Elem() != reflect.TypeFor[T]() { return nil } - cpy := make([]string, rv.Len()) + cpy := make([]T, rv.Len()) if len(cpy) > 0 { _ = reflect.Copy(reflect.ValueOf(cpy), rv) } diff --git a/vendor/go.opentelemetry.io/otel/attribute/kv.go b/vendor/go.opentelemetry.io/otel/attribute/kv.go index 8c6928ca..0cc36801 100644 --- a/vendor/go.opentelemetry.io/otel/attribute/kv.go +++ b/vendor/go.opentelemetry.io/otel/attribute/kv.go @@ -15,7 +15,7 @@ type KeyValue struct { // Valid reports whether kv is a valid OpenTelemetry attribute. func (kv KeyValue) Valid() bool { - return kv.Key.Defined() && kv.Value.Type() != INVALID + return kv.Key.Defined() } // Bool creates a KeyValue with a BOOL Value type. diff --git a/vendor/go.opentelemetry.io/otel/attribute/type_string.go b/vendor/go.opentelemetry.io/otel/attribute/type_string.go index 24f1fa37..6c04448d 100644 --- a/vendor/go.opentelemetry.io/otel/attribute/type_string.go +++ b/vendor/go.opentelemetry.io/otel/attribute/type_string.go @@ -8,7 +8,7 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[INVALID-0] + _ = x[EMPTY-0] _ = x[BOOL-1] _ = x[INT64-2] _ = x[FLOAT64-3] @@ -19,9 +19,9 @@ func _() { _ = x[STRINGSLICE-8] } -const _Type_name = "INVALIDBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE" +const _Type_name = "EMPTYBOOLINT64FLOAT64STRINGBOOLSLICEINT64SLICEFLOAT64SLICESTRINGSLICE" -var _Type_index = [...]uint8{0, 7, 11, 16, 23, 29, 38, 48, 60, 71} +var _Type_index = [...]uint8{0, 5, 9, 14, 21, 27, 36, 46, 58, 69} func (i Type) String() string { idx := int(i) - 0 diff --git a/vendor/go.opentelemetry.io/otel/attribute/value.go b/vendor/go.opentelemetry.io/otel/attribute/value.go index 5931e712..db04b132 100644 --- a/vendor/go.opentelemetry.io/otel/attribute/value.go +++ b/vendor/go.opentelemetry.io/otel/attribute/value.go @@ -6,7 +6,6 @@ package attribute // import "go.opentelemetry.io/otel/attribute" import ( "encoding/json" "fmt" - "reflect" "strconv" attribute "go.opentelemetry.io/otel/attribute/internal" @@ -18,6 +17,8 @@ import ( type Type int // nolint: revive // redefines builtin Type. // Value represents the value part in key-value pairs. +// +// Note that the zero value is a valid empty value. type Value struct { vtype Type numeric uint64 @@ -26,8 +27,8 @@ type Value struct { } const ( - // INVALID is used for a Value with no value set. - INVALID Type = iota + // EMPTY is used for a Value with no value set. + EMPTY Type = iota // BOOL is a boolean Type Value. BOOL // INT64 is a 64-bit signed integral Type Value. @@ -44,6 +45,10 @@ const ( FLOAT64SLICE // STRINGSLICE is a slice of strings Type Value. STRINGSLICE + // INVALID is used for a Value with no value set. + // + // Deprecated: Use EMPTY instead as an empty value is a valid value. + INVALID = EMPTY ) // BoolValue creates a BOOL Value. @@ -56,7 +61,7 @@ func BoolValue(v bool) Value { // BoolSliceValue creates a BOOLSLICE Value. func BoolSliceValue(v []bool) Value { - return Value{vtype: BOOLSLICE, slice: attribute.BoolSliceValue(v)} + return Value{vtype: BOOLSLICE, slice: attribute.SliceValue(v)} } // IntValue creates an INT64 Value. @@ -64,16 +69,30 @@ func IntValue(v int) Value { return Int64Value(int64(v)) } -// IntSliceValue creates an INTSLICE Value. +// IntSliceValue creates an INT64SLICE Value. func IntSliceValue(v []int) Value { - cp := reflect.New(reflect.ArrayOf(len(v), reflect.TypeFor[int64]())) - for i, val := range v { - cp.Elem().Index(i).SetInt(int64(val)) - } - return Value{ - vtype: INT64SLICE, - slice: cp.Elem().Interface(), + val := Value{vtype: INT64SLICE} + + // Avoid the common tiny-slice cases from allocating a new slice. + switch len(v) { + case 0: + val.slice = [0]int64{} + case 1: + val.slice = [1]int64{int64(v[0])} + case 2: + val.slice = [2]int64{int64(v[0]), int64(v[1])} + case 3: + val.slice = [3]int64{int64(v[0]), int64(v[1]), int64(v[2])} + default: + // Fallback to a new slice for larger slices. + cp := make([]int64, len(v)) + for i, val := range v { + cp[i] = int64(val) + } + val.slice = attribute.SliceValue(cp) } + + return val } // Int64Value creates an INT64 Value. @@ -86,7 +105,7 @@ func Int64Value(v int64) Value { // Int64SliceValue creates an INT64SLICE Value. func Int64SliceValue(v []int64) Value { - return Value{vtype: INT64SLICE, slice: attribute.Int64SliceValue(v)} + return Value{vtype: INT64SLICE, slice: attribute.SliceValue(v)} } // Float64Value creates a FLOAT64 Value. @@ -99,7 +118,7 @@ func Float64Value(v float64) Value { // Float64SliceValue creates a FLOAT64SLICE Value. func Float64SliceValue(v []float64) Value { - return Value{vtype: FLOAT64SLICE, slice: attribute.Float64SliceValue(v)} + return Value{vtype: FLOAT64SLICE, slice: attribute.SliceValue(v)} } // StringValue creates a STRING Value. @@ -112,7 +131,7 @@ func StringValue(v string) Value { // StringSliceValue creates a STRINGSLICE Value. func StringSliceValue(v []string) Value { - return Value{vtype: STRINGSLICE, slice: attribute.StringSliceValue(v)} + return Value{vtype: STRINGSLICE, slice: attribute.SliceValue(v)} } // Type returns a type of the Value. @@ -136,7 +155,7 @@ func (v Value) AsBoolSlice() []bool { } func (v Value) asBoolSlice() []bool { - return attribute.AsBoolSlice(v.slice) + return attribute.AsSlice[bool](v.slice) } // AsInt64 returns the int64 value. Make sure that the Value's type is @@ -155,7 +174,7 @@ func (v Value) AsInt64Slice() []int64 { } func (v Value) asInt64Slice() []int64 { - return attribute.AsInt64Slice(v.slice) + return attribute.AsSlice[int64](v.slice) } // AsFloat64 returns the float64 value. Make sure that the Value's @@ -174,7 +193,7 @@ func (v Value) AsFloat64Slice() []float64 { } func (v Value) asFloat64Slice() []float64 { - return attribute.AsFloat64Slice(v.slice) + return attribute.AsSlice[float64](v.slice) } // AsString returns the string value. Make sure that the Value's type @@ -193,7 +212,7 @@ func (v Value) AsStringSlice() []string { } func (v Value) asStringSlice() []string { - return attribute.AsStringSlice(v.slice) + return attribute.AsSlice[string](v.slice) } type unknownValueType struct{} @@ -217,6 +236,8 @@ func (v Value) AsInterface() any { return v.stringly case STRINGSLICE: return v.asStringSlice() + case EMPTY: + return nil } return unknownValueType{} } @@ -252,6 +273,8 @@ func (v Value) Emit() string { return string(j) case STRING: return v.stringly + case EMPTY: + return "" default: return "unknown" } diff --git a/vendor/go.opentelemetry.io/otel/dependencies.Dockerfile b/vendor/go.opentelemetry.io/otel/dependencies.Dockerfile index f0cc942b..7a9b3c05 100644 --- a/vendor/go.opentelemetry.io/otel/dependencies.Dockerfile +++ b/vendor/go.opentelemetry.io/otel/dependencies.Dockerfile @@ -1,4 +1,4 @@ # This is a renovate-friendly source of Docker images. FROM python:3.13.6-slim-bullseye@sha256:e98b521460ee75bca92175c16247bdf7275637a8faaeb2bcfa19d879ae5c4b9a AS python -FROM otel/weaver:v0.21.2@sha256:2401de985c38bdb98b43918e2f43aa36b2afed4aa5669ac1c1de0a17301cd36d AS weaver +FROM otel/weaver:v0.22.1@sha256:33ae522ae4b71c1c562563c1d81f46aa0f79f088a0873199143a1f11ac30e5c9 AS weaver FROM avtodev/markdown-lint:v1@sha256:6aeedc2f49138ce7a1cd0adffc1b1c0321b841dc2102408967d9301c031949ee AS markdown diff --git a/vendor/go.opentelemetry.io/otel/propagation/trace_context.go b/vendor/go.opentelemetry.io/otel/propagation/trace_context.go index 271ab71f..11f404de 100644 --- a/vendor/go.opentelemetry.io/otel/propagation/trace_context.go +++ b/vendor/go.opentelemetry.io/otel/propagation/trace_context.go @@ -46,8 +46,8 @@ func (TraceContext) Inject(ctx context.Context, carrier TextMapCarrier) { carrier.Set(tracestateHeader, ts) } - // Clear all flags other than the trace-context supported sampling bit. - flags := sc.TraceFlags() & trace.FlagsSampled + // Preserve only the spec-defined flags: sampled (0x01) and random (0x02). + flags := sc.TraceFlags() & (trace.FlagsSampled | trace.FlagsRandom) var sb strings.Builder sb.Grow(2 + 32 + 16 + 2 + 3) @@ -104,14 +104,13 @@ func (TraceContext) extract(carrier TextMapCarrier) trace.SpanContext { if !extractPart(opts[:], &h, 2) { return trace.SpanContext{} } - if version == 0 && (h != "" || opts[0] > 2) { - // version 0 not allow extra - // version 0 not allow other flag + if version == 0 && (h != "" || opts[0] > 3) { + // version 0 does not allow extra fields or reserved flag bits. return trace.SpanContext{} } - // Clear all flags other than the trace-context supported sampling bit. - scc.TraceFlags = trace.TraceFlags(opts[0]) & trace.FlagsSampled // nolint:gosec // slice size already checked. + scc.TraceFlags = trace.TraceFlags(opts[0]) & //nolint:gosec // slice size already checked. + (trace.FlagsSampled | trace.FlagsRandom) // Ignore the error returned here. Failure to parse tracestate MUST NOT // affect the parsing of traceparent according to the W3C tracecontext diff --git a/vendor/go.opentelemetry.io/otel/requirements.txt b/vendor/go.opentelemetry.io/otel/requirements.txt index 1bb55fb1..7c541dee 100644 --- a/vendor/go.opentelemetry.io/otel/requirements.txt +++ b/vendor/go.opentelemetry.io/otel/requirements.txt @@ -1 +1 @@ -codespell==2.4.1 +codespell==2.4.2 diff --git a/vendor/go.opentelemetry.io/otel/sdk/internal/x/features.go b/vendor/go.opentelemetry.io/otel/sdk/internal/x/features.go index bfeb73e8..694b64a3 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/internal/x/features.go +++ b/vendor/go.opentelemetry.io/otel/sdk/internal/x/features.go @@ -37,3 +37,18 @@ var Observability = newFeature( return "", false }, ) + +// PerSeriesStartTimestamps is an experimental feature flag that determines if the SDK +// uses the new Start Timestamps specification. +// +// To enable this feature set the OTEL_GO_X_PER_SERIES_START_TIMESTAMPS environment variable +// to the case-insensitive string value of "true". +var PerSeriesStartTimestamps = newFeature( + []string{"PER_SERIES_START_TIMESTAMPS"}, + func(v string) (bool, bool) { + if strings.EqualFold(v, "true") { + return true, true + } + return false, false + }, +) diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/builtin.go b/vendor/go.opentelemetry.io/otel/sdk/resource/builtin.go index 8a7bb330..04f15fcd 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/builtin.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/builtin.go @@ -13,7 +13,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/sdk" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) type ( diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/config.go b/vendor/go.opentelemetry.io/otel/sdk/resource/config.go index 0d6e213d..a3d647d9 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/config.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/config.go @@ -193,3 +193,11 @@ func WithContainer() Option { func WithContainerID() Option { return WithDetectors(cgroupContainerIDDetector{}) } + +// WithService adds all the Service attributes to the configured Resource. +func WithService() Option { + return WithDetectors( + defaultServiceInstanceIDDetector{}, + defaultServiceNameDetector{}, + ) +} diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/container.go b/vendor/go.opentelemetry.io/otel/sdk/resource/container.go index a19b39de..e977ff1c 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/container.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/container.go @@ -11,7 +11,7 @@ import ( "os" "regexp" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) type containerIDProvider func() (string, error) diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/env.go b/vendor/go.opentelemetry.io/otel/sdk/resource/env.go index c4915722..bc0e5c19 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/env.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/env.go @@ -12,7 +12,7 @@ import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) const ( diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/host_id.go b/vendor/go.opentelemetry.io/otel/sdk/resource/host_id.go index 023621ba..755c0824 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/host_id.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/host_id.go @@ -8,7 +8,7 @@ import ( "errors" "strings" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) type hostIDProvider func() (string, error) @@ -31,19 +31,19 @@ type hostIDReaderBSD struct { readFile fileReader } -// read attempts to read the machine-id from /etc/hostid. If not found it will -// execute `kenv -q smbios.system.uuid`. If neither location yields an id an -// error will be returned. +// read attempts to read the machine-id from /etc/hostid. +// If not found it will execute: /bin/kenv -q smbios.system.uuid. +// If neither location yields an id an error will be returned. func (r *hostIDReaderBSD) read() (string, error) { if result, err := r.readFile("/etc/hostid"); err == nil { return strings.TrimSpace(result), nil } - if result, err := r.execCommand("kenv", "-q", "smbios.system.uuid"); err == nil { + if result, err := r.execCommand("/bin/kenv", "-q", "smbios.system.uuid"); err == nil { return strings.TrimSpace(result), nil } - return "", errors.New("host id not found in: /etc/hostid or kenv") + return "", errors.New("host id not found in: /etc/hostid or /bin/kenv") } // hostIDReaderDarwin implements hostIDReader. diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/host_id_readfile.go b/vendor/go.opentelemetry.io/otel/sdk/resource/host_id_readfile.go index 6354b356..c95d8768 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/host_id_readfile.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/host_id_readfile.go @@ -8,7 +8,7 @@ package resource // import "go.opentelemetry.io/otel/sdk/resource" import "os" func readFile(filename string) (string, error) { - b, err := os.ReadFile(filename) + b, err := os.ReadFile(filename) // nolint:gosec // false positive if err != nil { return "", err } diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/os.go b/vendor/go.opentelemetry.io/otel/sdk/resource/os.go index 534809e2..f5682cad 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/os.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/os.go @@ -8,7 +8,7 @@ import ( "strings" "go.opentelemetry.io/otel/attribute" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) type osDescriptionProvider func() (string, error) diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/process.go b/vendor/go.opentelemetry.io/otel/sdk/resource/process.go index a1189553..99dce64f 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/process.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/process.go @@ -11,7 +11,7 @@ import ( "path/filepath" "runtime" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" ) type ( diff --git a/vendor/go.opentelemetry.io/otel/sdk/resource/resource.go b/vendor/go.opentelemetry.io/otel/sdk/resource/resource.go index 28e1e4f7..f715be53 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/resource/resource.go +++ b/vendor/go.opentelemetry.io/otel/sdk/resource/resource.go @@ -232,6 +232,15 @@ func Empty() *Resource { // Default returns an instance of Resource with a default // "service.name" and OpenTelemetrySDK attributes. func Default() *Resource { + return DefaultWithContext(context.Background()) +} + +// DefaultWithContext returns an instance of Resource with a default +// "service.name" and OpenTelemetrySDK attributes. +// +// If the default resource has already been initialized, the provided ctx +// is ignored and the cached resource is returned. +func DefaultWithContext(ctx context.Context) *Resource { defaultResourceOnce.Do(func() { var err error defaultDetectors := []Detector{ @@ -243,7 +252,7 @@ func Default() *Resource { defaultDetectors = append([]Detector{defaultServiceInstanceIDDetector{}}, defaultDetectors...) } defaultResource, err = Detect( - context.Background(), + ctx, defaultDetectors..., ) if err != nil { @@ -260,8 +269,14 @@ func Default() *Resource { // Environment returns an instance of Resource with attributes // extracted from the OTEL_RESOURCE_ATTRIBUTES environment variable. func Environment() *Resource { + return EnvironmentWithContext(context.Background()) +} + +// EnvironmentWithContext returns an instance of Resource with attributes +// extracted from the OTEL_RESOURCE_ATTRIBUTES environment variable. +func EnvironmentWithContext(ctx context.Context) *Resource { detector := &fromEnv{} - resource, err := detector.Detect(context.Background()) + resource, err := detector.Detect(ctx) if err != nil { otel.Handle(err) } diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/batch_span_processor.go b/vendor/go.opentelemetry.io/otel/sdk/trace/batch_span_processor.go index 7d15cbb9..32854b14 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/trace/batch_span_processor.go +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/batch_span_processor.go @@ -68,7 +68,7 @@ type batchSpanProcessor struct { o BatchSpanProcessorOptions queue chan ReadOnlySpan - dropped uint32 + dropped atomic.Uint32 inst *observ.BSP @@ -123,12 +123,10 @@ func NewBatchSpanProcessor(exporter SpanExporter, options ...BatchSpanProcessorO otel.Handle(err) } - bsp.stopWait.Add(1) - go func() { - defer bsp.stopWait.Done() + bsp.stopWait.Go(func() { bsp.processQueue() bsp.drainQueue() - }() + }) return bsp } @@ -295,7 +293,7 @@ func (bsp *batchSpanProcessor) exportSpans(ctx context.Context) error { } if l := len(bsp.batch); l > 0 { - global.Debug("exporting spans", "count", len(bsp.batch), "total_dropped", atomic.LoadUint32(&bsp.dropped)) + global.Debug("exporting spans", "count", len(bsp.batch), "total_dropped", bsp.dropped.Load()) if bsp.inst != nil { bsp.inst.Processed(ctx, int64(l)) } @@ -423,7 +421,7 @@ func (bsp *batchSpanProcessor) enqueueDrop(ctx context.Context, sd ReadOnlySpan) case bsp.queue <- sd: return true default: - atomic.AddUint32(&bsp.dropped, 1) + bsp.dropped.Add(1) if bsp.inst != nil { bsp.inst.ProcessedQueueFull(ctx, 1) } diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/batch_span_processor.go b/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/batch_span_processor.go index d9cfba0b..c31e03aa 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/batch_span_processor.go +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/batch_span_processor.go @@ -13,8 +13,8 @@ import ( "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/internal/x" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" - "go.opentelemetry.io/otel/semconv/v1.39.0/otelconv" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" + "go.opentelemetry.io/otel/semconv/v1.40.0/otelconv" ) const ( diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/simple_span_processor.go b/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/simple_span_processor.go index 8afd0526..0e77cd95 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/simple_span_processor.go +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/simple_span_processor.go @@ -13,8 +13,8 @@ import ( "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/internal/x" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" - "go.opentelemetry.io/otel/semconv/v1.39.0/otelconv" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" + "go.opentelemetry.io/otel/semconv/v1.40.0/otelconv" ) var measureAttrsPool = sync.Pool{ diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/tracer.go b/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/tracer.go index 13a2db29..560d316f 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/tracer.go +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/internal/observ/tracer.go @@ -13,7 +13,7 @@ import ( "go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/sdk" "go.opentelemetry.io/otel/sdk/internal/x" - "go.opentelemetry.io/otel/semconv/v1.39.0/otelconv" + "go.opentelemetry.io/otel/semconv/v1.40.0/otelconv" "go.opentelemetry.io/otel/trace" ) @@ -55,6 +55,10 @@ func NewTracer() (Tracer, error) { func (t Tracer) Enabled() bool { return t.enabled } func (t Tracer) SpanStarted(ctx context.Context, psc trace.SpanContext, span trace.Span) { + if !t.started.Enabled(ctx) { + return + } + key := spanStartedKey{ parent: parentStateNoParent, sampling: samplingStateDrop, @@ -89,6 +93,10 @@ func (t Tracer) SpanEnded(ctx context.Context, span trace.Span) { } func (t Tracer) spanLive(ctx context.Context, value int64, span trace.Span) { + if !t.live.Enabled(ctx) { + return + } + key := spanLiveKey{sampled: span.SpanContext().IsSampled()} opts := spanLiveOpts[key] t.live.Add(ctx, value, opts...) diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/provider.go b/vendor/go.opentelemetry.io/otel/sdk/trace/provider.go index d2cf4ebd..cd40d299 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/trace/provider.go +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/provider.go @@ -5,6 +5,7 @@ package trace // import "go.opentelemetry.io/otel/sdk/trace" import ( "context" + "errors" "fmt" "sync" "sync/atomic" @@ -262,6 +263,7 @@ func (p *TracerProvider) ForceFlush(ctx context.Context) error { return nil } + var err error for _, sps := range spss { select { case <-ctx.Done(): @@ -269,11 +271,9 @@ func (p *TracerProvider) ForceFlush(ctx context.Context) error { default: } - if err := sps.sp.ForceFlush(ctx); err != nil { - return err - } + err = errors.Join(err, sps.sp.ForceFlush(ctx)) } - return nil + return err } // Shutdown shuts down TracerProvider. All registered span processors are shut down @@ -303,14 +303,7 @@ func (p *TracerProvider) Shutdown(ctx context.Context) error { sps.state.Do(func() { err = sps.sp.Shutdown(ctx) }) - if err != nil { - if retErr == nil { - retErr = err - } else { - // Poor man's list of errors - retErr = fmt.Errorf("%w; %w", retErr, err) - } - } + retErr = errors.Join(retErr, err) } p.spanProcessors.Store(&spanProcessorStates{}) return retErr diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/sampling.go b/vendor/go.opentelemetry.io/otel/sdk/trace/sampling.go index 81c5060a..845e292c 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/trace/sampling.go +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/sampling.go @@ -69,17 +69,17 @@ type traceIDRatioSampler struct { } func (ts traceIDRatioSampler) ShouldSample(p SamplingParameters) SamplingResult { - psc := trace.SpanContextFromContext(p.ParentContext) + state := trace.SpanContextFromContext(p.ParentContext).TraceState() x := binary.BigEndian.Uint64(p.TraceID[8:16]) >> 1 if x < ts.traceIDUpperBound { return SamplingResult{ Decision: RecordAndSample, - Tracestate: psc.TraceState(), + Tracestate: state, } } return SamplingResult{ Decision: Drop, - Tracestate: psc.TraceState(), + Tracestate: state, } } @@ -94,12 +94,20 @@ func (ts traceIDRatioSampler) Description() string { // //nolint:revive // revive complains about stutter of `trace.TraceIDRatioBased` func TraceIDRatioBased(fraction float64) Sampler { + // Cannot use AlwaysSample() and NeverSample(), must return spec-compliant descriptions. + // See https://opentelemetry.io/docs/specs/otel/trace/sdk/#traceidratiobased. if fraction >= 1 { - return AlwaysSample() + return predeterminedSampler{ + description: "TraceIDRatioBased{1}", + decision: RecordAndSample, + } } if fraction <= 0 { - fraction = 0 + return predeterminedSampler{ + description: "TraceIDRatioBased{0}", + decision: Drop, + } } return &traceIDRatioSampler{ @@ -118,6 +126,7 @@ func (alwaysOnSampler) ShouldSample(p SamplingParameters) SamplingResult { } func (alwaysOnSampler) Description() string { + // https://opentelemetry.io/docs/specs/otel/trace/sdk/#alwayson return "AlwaysOnSampler" } @@ -139,6 +148,7 @@ func (alwaysOffSampler) ShouldSample(p SamplingParameters) SamplingResult { } func (alwaysOffSampler) Description() string { + // https://opentelemetry.io/docs/specs/otel/trace/sdk/#alwaysoff return "AlwaysOffSampler" } @@ -147,6 +157,22 @@ func NeverSample() Sampler { return alwaysOffSampler{} } +type predeterminedSampler struct { + description string + decision SamplingDecision +} + +func (s predeterminedSampler) ShouldSample(p SamplingParameters) SamplingResult { + return SamplingResult{ + Decision: s.decision, + Tracestate: trace.SpanContextFromContext(p.ParentContext).TraceState(), + } +} + +func (s predeterminedSampler) Description() string { + return s.description +} + // ParentBased returns a sampler decorator which behaves differently, // based on the parent of the span. If the span has no parent, // the decorated sampler is used to make sampling decision. If the span has diff --git a/vendor/go.opentelemetry.io/otel/sdk/trace/span.go b/vendor/go.opentelemetry.io/otel/sdk/trace/span.go index d4666105..7d55ce1d 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/trace/span.go +++ b/vendor/go.opentelemetry.io/otel/sdk/trace/span.go @@ -20,7 +20,7 @@ import ( "go.opentelemetry.io/otel/internal/global" "go.opentelemetry.io/otel/sdk/instrumentation" "go.opentelemetry.io/otel/sdk/resource" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" "go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace/embedded" ) diff --git a/vendor/go.opentelemetry.io/otel/sdk/version.go b/vendor/go.opentelemetry.io/otel/sdk/version.go index b5497c28..766731dd 100644 --- a/vendor/go.opentelemetry.io/otel/sdk/version.go +++ b/vendor/go.opentelemetry.io/otel/sdk/version.go @@ -6,5 +6,5 @@ package sdk // import "go.opentelemetry.io/otel/sdk" // Version is the current release version of the OpenTelemetry SDK in use. func Version() string { - return "1.40.0" + return "1.43.0" } diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/MIGRATION.md b/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/MIGRATION.md deleted file mode 100644 index fed7013e..00000000 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/MIGRATION.md +++ /dev/null @@ -1,78 +0,0 @@ - -# Migration from v1.38.0 to v1.39.0 - -The `go.opentelemetry.io/otel/semconv/v1.39.0` package should be a drop-in replacement for `go.opentelemetry.io/otel/semconv/v1.38.0` with the following exceptions. - -## Removed - -The following declarations have been removed. -Refer to the [OpenTelemetry Semantic Conventions documentation] for deprecation instructions. - -If the type is not listed in the documentation as deprecated, it has been removed in this version due to lack of applicability or use. -If you use any of these non-deprecated declarations in your Go application, please [open an issue] describing your use-case. - -- `LinuxMemorySlabStateKey` -- `LinuxMemorySlabStateReclaimable` -- `LinuxMemorySlabStateUnreclaimable` -- `PeerService` -- `PeerServiceKey` -- `RPCConnectRPCErrorCodeAborted` -- `RPCConnectRPCErrorCodeAlreadyExists` -- `RPCConnectRPCErrorCodeCancelled` -- `RPCConnectRPCErrorCodeDataLoss` -- `RPCConnectRPCErrorCodeDeadlineExceeded` -- `RPCConnectRPCErrorCodeFailedPrecondition` -- `RPCConnectRPCErrorCodeInternal` -- `RPCConnectRPCErrorCodeInvalidArgument` -- `RPCConnectRPCErrorCodeKey` -- `RPCConnectRPCErrorCodeNotFound` -- `RPCConnectRPCErrorCodeOutOfRange` -- `RPCConnectRPCErrorCodePermissionDenied` -- `RPCConnectRPCErrorCodeResourceExhausted` -- `RPCConnectRPCErrorCodeUnauthenticated` -- `RPCConnectRPCErrorCodeUnavailable` -- `RPCConnectRPCErrorCodeUnimplemented` -- `RPCConnectRPCErrorCodeUnknown` -- `RPCConnectRPCRequestMetadata` -- `RPCConnectRPCResponseMetadata` -- `RPCGRPCRequestMetadata` -- `RPCGRPCResponseMetadata` -- `RPCGRPCStatusCodeAborted` -- `RPCGRPCStatusCodeAlreadyExists` -- `RPCGRPCStatusCodeCancelled` -- `RPCGRPCStatusCodeDataLoss` -- `RPCGRPCStatusCodeDeadlineExceeded` -- `RPCGRPCStatusCodeFailedPrecondition` -- `RPCGRPCStatusCodeInternal` -- `RPCGRPCStatusCodeInvalidArgument` -- `RPCGRPCStatusCodeKey` -- `RPCGRPCStatusCodeNotFound` -- `RPCGRPCStatusCodeOk` -- `RPCGRPCStatusCodeOutOfRange` -- `RPCGRPCStatusCodePermissionDenied` -- `RPCGRPCStatusCodeResourceExhausted` -- `RPCGRPCStatusCodeUnauthenticated` -- `RPCGRPCStatusCodeUnavailable` -- `RPCGRPCStatusCodeUnimplemented` -- `RPCGRPCStatusCodeUnknown` -- `RPCJSONRPCErrorCode` -- `RPCJSONRPCErrorCodeKey` -- `RPCJSONRPCErrorMessage` -- `RPCJSONRPCErrorMessageKey` -- `RPCJSONRPCRequestID` -- `RPCJSONRPCRequestIDKey` -- `RPCJSONRPCVersion` -- `RPCJSONRPCVersionKey` -- `RPCService` -- `RPCServiceKey` -- `RPCSystemApacheDubbo` -- `RPCSystemConnectRPC` -- `RPCSystemDotnetWcf` -- `RPCSystemGRPC` -- `RPCSystemJSONRPC` -- `RPCSystemJavaRmi` -- `RPCSystemKey` -- `RPCSystemOncRPC` - -[OpenTelemetry Semantic Conventions documentation]: https://github.com/open-telemetry/semantic-conventions -[open an issue]: https://github.com/open-telemetry/opentelemetry-go/issues/new?template=Blank+issue diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/README.md b/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/README.md deleted file mode 100644 index 4b0e6f7f..00000000 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Semconv v1.39.0 - -[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/semconv/v1.39.0)](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.39.0) diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/MIGRATION.md b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/MIGRATION.md new file mode 100644 index 00000000..e246b169 --- /dev/null +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/MIGRATION.md @@ -0,0 +1,27 @@ + +# Migration from v1.39.0 to v1.40.0 + +The `go.opentelemetry.io/otel/semconv/v1.40.0` package should be a drop-in replacement for `go.opentelemetry.io/otel/semconv/v1.39.0` with the following exceptions. + +## Removed + +The following declarations have been removed. +Refer to the [OpenTelemetry Semantic Conventions documentation] for deprecation instructions. + +If the type is not listed in the documentation as deprecated, it has been removed in this version due to lack of applicability or use. +If you use any of these non-deprecated declarations in your Go application, please [open an issue] describing your use-case. + +- `ErrorMessage` +- `ErrorMessageKey` +- `RPCMessageCompressedSize` +- `RPCMessageCompressedSizeKey` +- `RPCMessageID` +- `RPCMessageIDKey` +- `RPCMessageTypeKey` +- `RPCMessageTypeReceived` +- `RPCMessageTypeSent` +- `RPCMessageUncompressedSize` +- `RPCMessageUncompressedSizeKey` + +[OpenTelemetry Semantic Conventions documentation]: https://github.com/open-telemetry/semantic-conventions +[open an issue]: https://github.com/open-telemetry/opentelemetry-go/issues/new?template=Blank+issue diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/README.md b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/README.md new file mode 100644 index 00000000..c51b7fb7 --- /dev/null +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/README.md @@ -0,0 +1,3 @@ +# Semconv v1.40.0 + +[![PkgGoDev](https://pkg.go.dev/badge/go.opentelemetry.io/otel/semconv/v1.40.0)](https://pkg.go.dev/go.opentelemetry.io/otel/semconv/v1.40.0) diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/attribute_group.go b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/attribute_group.go similarity index 95% rename from vendor/go.opentelemetry.io/otel/semconv/v1.39.0/attribute_group.go rename to vendor/go.opentelemetry.io/otel/semconv/v1.40.0/attribute_group.go index 080365fc..ee6b1f79 100644 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/attribute_group.go +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/attribute_group.go @@ -3,7 +3,7 @@ // Code generated from semantic convention specification. DO NOT EDIT. -package semconv // import "go.opentelemetry.io/otel/semconv/v1.39.0" +package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0" import "go.opentelemetry.io/otel/attribute" @@ -3431,7 +3431,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "a3bf90e006b2" // @@ -3467,7 +3467,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "gcr.io/opentelemetry/operator" ContainerImageNameKey = attribute.Key("container.image.name") @@ -3478,7 +3478,7 @@ const ( // // Type: string[] // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: // "example@sha256:afcc7f1ac1b49db317a7196c902e61c6c3c4607d63599ee1a82d702d249a0ccb", @@ -3497,7 +3497,7 @@ const ( // // Type: string[] // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "v1.27.1", "3.5.7-0" // @@ -3856,6 +3856,12 @@ const ( // [Generating query summary] // section. // + // For batch operations, if the individual operations are known to have the same + // query summary + // then that query summary SHOULD be used prepended by `BATCH `, + // otherwise `db.query.summary` SHOULD be `BATCH` or some other database + // system specific term if more applicable. + // // [Generating query summary]: /docs/db/database-spans.md#generating-a-summary-of-the-query DBQuerySummaryKey = attribute.Key("db.query.summary") @@ -4623,27 +4629,6 @@ func EnduserPseudoID(val string) attribute.KeyValue { // Namespace: error const ( - // ErrorMessageKey is the attribute Key conforming to the "error.message" - // semantic conventions. It represents a message providing more detail about an - // error in human-readable form. - // - // Type: string - // RequirementLevel: Recommended - // Stability: Development - // - // Examples: "Unexpected input type: string", "The user has exceeded their - // storage quota" - // Note: `error.message` should provide additional context and detail about an - // error. - // It is NOT RECOMMENDED to duplicate the value of `error.type` in - // `error.message`. - // It is also NOT RECOMMENDED to duplicate the value of `exception.message` in - // `error.message`. - // - // `error.message` is NOT RECOMMENDED for metrics or spans due to its unbounded - // cardinality and overlap with span status. - ErrorMessageKey = attribute.Key("error.message") - // ErrorTypeKey is the attribute Key conforming to the "error.type" semantic // conventions. It represents the describes a class of error the operation ended // with. @@ -4683,13 +4668,6 @@ const ( ErrorTypeKey = attribute.Key("error.type") ) -// ErrorMessage returns an attribute KeyValue conforming to the "error.message" -// semantic conventions. It represents a message providing more detail about an -// error in human-readable form. -func ErrorMessage(val string) attribute.KeyValue { - return ErrorMessageKey.String(val) -} - // Enum values for error.type var ( // A fallback error value to be used when the instrumentation doesn't define a @@ -4710,6 +4688,9 @@ const ( // Stability: Stable // // Examples: "Division by zero", "Can't convert 'int' object to str implicitly" + // Note: > [!WARNING] + // + // > This attribute may contain sensitive information. ExceptionMessageKey = attribute.Key("exception.message") // ExceptionStacktraceKey is the attribute Key conforming to the @@ -5165,6 +5146,19 @@ const ( // Examples: "5157782b-2203-4c80-a857-dbbd5e7761db" FeatureFlagContextIDKey = attribute.Key("feature_flag.context.id") + // FeatureFlagErrorMessageKey is the attribute Key conforming to the + // "feature_flag.error.message" semantic conventions. It represents a message + // providing more detail about an error that occurred during feature flag + // evaluation in human-readable form. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Release_Candidate + // + // Examples: "Unexpected input type: string", "The user has exceeded their + // storage quota" + FeatureFlagErrorMessageKey = attribute.Key("feature_flag.error.message") + // FeatureFlagKeyKey is the attribute Key conforming to the "feature_flag.key" // semantic conventions. It represents the lookup key of the feature flag. // @@ -5266,6 +5260,14 @@ func FeatureFlagContextID(val string) attribute.KeyValue { return FeatureFlagContextIDKey.String(val) } +// FeatureFlagErrorMessage returns an attribute KeyValue conforming to the +// "feature_flag.error.message" semantic conventions. It represents a message +// providing more detail about an error that occurred during feature flag +// evaluation in human-readable form. +func FeatureFlagErrorMessage(val string) attribute.KeyValue { + return FeatureFlagErrorMessageKey.String(val) +} + // FeatureFlagKey returns an attribute KeyValue conforming to the // "feature_flag.key" semantic conventions. It represents the lookup key of the // feature flag. @@ -5980,6 +5982,41 @@ const ( // // [default internal DNS name]: https://cloud.google.com/compute/docs/internal-dns#instance-fully-qualified-domain-names GCPGCEInstanceNameKey = attribute.Key("gcp.gce.instance.name") + + // GCPGCEInstanceGroupManagerNameKey is the attribute Key conforming to the + // "gcp.gce.instance_group_manager.name" semantic conventions. It represents the + // name of the Instance Group Manager (IGM) that manages this VM, if any. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "web-igm", "my-managed-group" + GCPGCEInstanceGroupManagerNameKey = attribute.Key("gcp.gce.instance_group_manager.name") + + // GCPGCEInstanceGroupManagerRegionKey is the attribute Key conforming to the + // "gcp.gce.instance_group_manager.region" semantic conventions. It represents + // the region of a **regional** Instance Group Manager (e.g., `us-central1`). + // Set this **only** when the IGM is regional. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "us-central1", "europe-west1" + GCPGCEInstanceGroupManagerRegionKey = attribute.Key("gcp.gce.instance_group_manager.region") + + // GCPGCEInstanceGroupManagerZoneKey is the attribute Key conforming to the + // "gcp.gce.instance_group_manager.zone" semantic conventions. It represents the + // zone of a **zonal** Instance Group Manager (e.g., `us-central1-a`). Set this + // **only** when the IGM is zonal. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "us-central1-a", "europe-west1-b" + GCPGCEInstanceGroupManagerZoneKey = attribute.Key("gcp.gce.instance_group_manager.zone") ) // GCPAppHubApplicationContainer returns an attribute KeyValue conforming to the @@ -6103,6 +6140,29 @@ func GCPGCEInstanceName(val string) attribute.KeyValue { return GCPGCEInstanceNameKey.String(val) } +// GCPGCEInstanceGroupManagerName returns an attribute KeyValue conforming to the +// "gcp.gce.instance_group_manager.name" semantic conventions. It represents the +// name of the Instance Group Manager (IGM) that manages this VM, if any. +func GCPGCEInstanceGroupManagerName(val string) attribute.KeyValue { + return GCPGCEInstanceGroupManagerNameKey.String(val) +} + +// GCPGCEInstanceGroupManagerRegion returns an attribute KeyValue conforming to +// the "gcp.gce.instance_group_manager.region" semantic conventions. It +// represents the region of a **regional** Instance Group Manager (e.g., +// `us-central1`). Set this **only** when the IGM is regional. +func GCPGCEInstanceGroupManagerRegion(val string) attribute.KeyValue { + return GCPGCEInstanceGroupManagerRegionKey.String(val) +} + +// GCPGCEInstanceGroupManagerZone returns an attribute KeyValue conforming to the +// "gcp.gce.instance_group_manager.zone" semantic conventions. It represents the +// zone of a **zonal** Instance Group Manager (e.g., `us-central1-a`). Set this +// **only** when the IGM is zonal. +func GCPGCEInstanceGroupManagerZone(val string) attribute.KeyValue { + return GCPGCEInstanceGroupManagerZoneKey.String(val) +} + // Enum values for gcp.apphub.service.criticality_type var ( // Mission critical service. @@ -6265,6 +6325,17 @@ const ( // Examples: "Math Tutor", "Fiction Writer" GenAIAgentNameKey = attribute.Key("gen_ai.agent.name") + // GenAIAgentVersionKey is the attribute Key conforming to the + // "gen_ai.agent.version" semantic conventions. It represents the version of the + // GenAI agent. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "1.0.0", "2025-05-01" + GenAIAgentVersionKey = attribute.Key("gen_ai.agent.version") + // GenAIConversationIDKey is the attribute Key conforming to the // "gen_ai.conversation.id" semantic conventions. It represents the unique // identifier for a conversation (session, thread), used to store and correlate @@ -6663,6 +6734,44 @@ const ( // Examples: "gpt-4-0613" GenAIResponseModelKey = attribute.Key("gen_ai.response.model") + // GenAIRetrievalDocumentsKey is the attribute Key conforming to the + // "gen_ai.retrieval.documents" semantic conventions. It represents the + // documents retrieved. + // + // Type: any + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "[\n {\n "id": "doc_123",\n "score": 0.95\n },\n {\n "id": + // "doc_456",\n "score": 0.87\n },\n {\n "id": "doc_789",\n "score": 0.82\n + // }\n]\n" + // Note: Instrumentations MUST follow [Retrieval documents JSON schema]. + // When the attribute is recorded on events, it MUST be recorded in structured + // form. When recorded on spans, it MAY be recorded as a JSON string if + // structured + // format is not supported and SHOULD be recorded in structured form otherwise. + // + // Each document object SHOULD contain at least the following properties: + // `id` (string): A unique identifier for the document, `score` (double): The + // relevance score of the document + // + // [Retrieval documents JSON schema]: /docs/gen-ai/gen-ai-retrieval-documents.json + GenAIRetrievalDocumentsKey = attribute.Key("gen_ai.retrieval.documents") + + // GenAIRetrievalQueryTextKey is the attribute Key conforming to the + // "gen_ai.retrieval.query.text" semantic conventions. It represents the query + // text used for retrieval. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "What is the capital of France?", "weather in Paris" + // Note: > [!Warning] + // + // > This attribute may contain sensitive information. + GenAIRetrievalQueryTextKey = attribute.Key("gen_ai.retrieval.query.text") + // GenAISystemInstructionsKey is the attribute Key conforming to the // "gen_ai.system_instructions" semantic conventions. It represents the system // message or instructions provided to the GenAI model separately from the chat @@ -6837,6 +6946,30 @@ const ( // updates. GenAIToolTypeKey = attribute.Key("gen_ai.tool.type") + // GenAIUsageCacheCreationInputTokensKey is the attribute Key conforming to the + // "gen_ai.usage.cache_creation.input_tokens" semantic conventions. It + // represents the number of input tokens written to a provider-managed cache. + // + // Type: int + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: 25 + // Note: The value SHOULD be included in `gen_ai.usage.input_tokens`. + GenAIUsageCacheCreationInputTokensKey = attribute.Key("gen_ai.usage.cache_creation.input_tokens") + + // GenAIUsageCacheReadInputTokensKey is the attribute Key conforming to the + // "gen_ai.usage.cache_read.input_tokens" semantic conventions. It represents + // the number of input tokens served from a provider-managed cache. + // + // Type: int + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: 50 + // Note: The value SHOULD be included in `gen_ai.usage.input_tokens`. + GenAIUsageCacheReadInputTokensKey = attribute.Key("gen_ai.usage.cache_read.input_tokens") + // GenAIUsageInputTokensKey is the attribute Key conforming to the // "gen_ai.usage.input_tokens" semantic conventions. It represents the number of // tokens used in the GenAI input (prompt). @@ -6846,6 +6979,12 @@ const ( // Stability: Development // // Examples: 100 + // Note: This value SHOULD include all types of input tokens, including cached + // tokens. + // Instrumentations SHOULD make a best effort to populate this value, using a + // total + // provided by the provider when available or, depending on the provider API, + // by summing different token types parsed from the provider output. GenAIUsageInputTokensKey = attribute.Key("gen_ai.usage.input_tokens") // GenAIUsageOutputTokensKey is the attribute Key conforming to the @@ -6880,6 +7019,13 @@ func GenAIAgentName(val string) attribute.KeyValue { return GenAIAgentNameKey.String(val) } +// GenAIAgentVersion returns an attribute KeyValue conforming to the +// "gen_ai.agent.version" semantic conventions. It represents the version of the +// GenAI agent. +func GenAIAgentVersion(val string) attribute.KeyValue { + return GenAIAgentVersionKey.String(val) +} + // GenAIConversationID returns an attribute KeyValue conforming to the // "gen_ai.conversation.id" semantic conventions. It represents the unique // identifier for a conversation (session, thread), used to store and correlate @@ -7036,6 +7182,13 @@ func GenAIResponseModel(val string) attribute.KeyValue { return GenAIResponseModelKey.String(val) } +// GenAIRetrievalQueryText returns an attribute KeyValue conforming to the +// "gen_ai.retrieval.query.text" semantic conventions. It represents the query +// text used for retrieval. +func GenAIRetrievalQueryText(val string) attribute.KeyValue { + return GenAIRetrievalQueryTextKey.String(val) +} + // GenAIToolCallID returns an attribute KeyValue conforming to the // "gen_ai.tool.call.id" semantic conventions. It represents the tool call // identifier. @@ -7064,6 +7217,20 @@ func GenAIToolType(val string) attribute.KeyValue { return GenAIToolTypeKey.String(val) } +// GenAIUsageCacheCreationInputTokens returns an attribute KeyValue conforming to +// the "gen_ai.usage.cache_creation.input_tokens" semantic conventions. It +// represents the number of input tokens written to a provider-managed cache. +func GenAIUsageCacheCreationInputTokens(val int) attribute.KeyValue { + return GenAIUsageCacheCreationInputTokensKey.Int(val) +} + +// GenAIUsageCacheReadInputTokens returns an attribute KeyValue conforming to the +// "gen_ai.usage.cache_read.input_tokens" semantic conventions. It represents the +// number of input tokens served from a provider-managed cache. +func GenAIUsageCacheReadInputTokens(val int) attribute.KeyValue { + return GenAIUsageCacheReadInputTokensKey.Int(val) +} + // GenAIUsageInputTokens returns an attribute KeyValue conforming to the // "gen_ai.usage.input_tokens" semantic conventions. It represents the number of // tokens used in the GenAI input (prompt). @@ -7100,6 +7267,11 @@ var ( // // [OpenAI Create embeddings API]: https://platform.openai.com/docs/api-reference/embeddings/create GenAIOperationNameEmbeddings = GenAIOperationNameKey.String("embeddings") + // Retrieval operation such as [OpenAI Search Vector Store API] + // Stability: development + // + // [OpenAI Search Vector Store API]: https://platform.openai.com/docs/api-reference/vector-stores/search + GenAIOperationNameRetrieval = GenAIOperationNameKey.String("retrieval") // Create GenAI agent // Stability: development GenAIOperationNameCreateAgent = GenAIOperationNameKey.String("create_agent") @@ -7889,9 +8061,18 @@ const ( // the list of known HTTP methods. If this override is done via environment // variable, then the environment variable MUST be named // OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of - // case-sensitive known HTTP methods - // (this list MUST be a full override of the default known method, it is not a - // list of known methods in addition to the defaults). + // case-sensitive known HTTP methods. + // + // + // If this override is done via declarative configuration, then the list MUST be + // configurable via the `known_methods` property + // (an array of case-sensitive strings with minimum items 0) under + // `.instrumentation/development.general.http.client` and/or + // `.instrumentation/development.general.http.server`. + // + // In either case, this list MUST be a full override of the default known + // methods, + // it is not a list of known methods in addition to the defaults. // // HTTP method names are case-sensitive and `http.request.method` attribute // value MUST match a known HTTP method name exactly. @@ -8845,7 +9026,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry-cluster" K8SClusterNameKey = attribute.Key("k8s.cluster.name") @@ -8856,7 +9037,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "218fc5a9-a5f1-4b54-aa05-46717d0ab26d" // Note: K8s doesn't have support for obtaining a cluster ID. If this is ever @@ -8892,7 +9073,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "redis" K8SContainerNameKey = attribute.Key("k8s.container.name") @@ -8904,7 +9085,7 @@ const ( // // Type: int // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: K8SContainerRestartCountKey = attribute.Key("k8s.container.restart_count") @@ -8955,7 +9136,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry" K8SCronJobNameKey = attribute.Key("k8s.cronjob.name") @@ -8965,7 +9146,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SCronJobUIDKey = attribute.Key("k8s.cronjob.uid") @@ -8976,7 +9157,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry" K8SDaemonSetNameKey = attribute.Key("k8s.daemonset.name") @@ -8986,7 +9167,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SDaemonSetUIDKey = attribute.Key("k8s.daemonset.uid") @@ -8997,7 +9178,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry" K8SDeploymentNameKey = attribute.Key("k8s.deployment.name") @@ -9008,7 +9189,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SDeploymentUIDKey = attribute.Key("k8s.deployment.uid") @@ -9098,7 +9279,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry" K8SJobNameKey = attribute.Key("k8s.job.name") @@ -9108,7 +9289,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SJobUIDKey = attribute.Key("k8s.job.uid") @@ -9119,7 +9300,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "default" K8SNamespaceNameKey = attribute.Key("k8s.namespace.name") @@ -9184,7 +9365,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "node-1" K8SNodeNameKey = attribute.Key("k8s.node.name") @@ -9194,7 +9375,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "1eb3a0c6-0477-4080-a9cb-0cb7db65c6a2" K8SNodeUIDKey = attribute.Key("k8s.node.uid") @@ -9204,7 +9385,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "collector-gateway" // Note: The K8s Pod spec has an optional hostname field, which can be used to @@ -9224,7 +9405,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "172.18.0.2" // Note: This attribute aligns with the `podIP` field of the @@ -9238,7 +9419,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry-pod-autoconf" K8SPodNameKey = attribute.Key("k8s.pod.name") @@ -9249,7 +9430,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "2025-12-04T08:41:03Z" // Note: Date and time at which the object was acknowledged by the Kubelet. @@ -9293,7 +9474,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SPodUIDKey = attribute.Key("k8s.pod.uid") @@ -9304,7 +9485,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry" K8SReplicaSetNameKey = attribute.Key("k8s.replicaset.name") @@ -9315,7 +9496,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SReplicaSetUIDKey = attribute.Key("k8s.replicaset.uid") @@ -9383,13 +9564,152 @@ const ( // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SResourceQuotaUIDKey = attribute.Key("k8s.resourcequota.uid") + // K8SServiceEndpointAddressTypeKey is the attribute Key conforming to the + // "k8s.service.endpoint.address_type" semantic conventions. It represents the + // address type of the service endpoint. + // + // Type: Enum + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "IPv4", "IPv6" + // Note: The network address family or type of the endpoint. + // This attribute aligns with the `addressType` field of the + // [K8s EndpointSlice]. + // It is used to differentiate metrics when a Service is backed by multiple + // address types + // (e.g., in dual-stack clusters). + // + // [K8s EndpointSlice]: https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/ + K8SServiceEndpointAddressTypeKey = attribute.Key("k8s.service.endpoint.address_type") + + // K8SServiceEndpointConditionKey is the attribute Key conforming to the + // "k8s.service.endpoint.condition" semantic conventions. It represents the + // condition of the service endpoint. + // + // Type: Enum + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "ready", "serving", "terminating" + // Note: The current operational condition of the service endpoint. + // An endpoint can have multiple conditions set at once (e.g., both `serving` + // and `terminating` during rollout). + // This attribute aligns with the condition fields in the [K8s EndpointSlice]. + // + // [K8s EndpointSlice]: https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/ + K8SServiceEndpointConditionKey = attribute.Key("k8s.service.endpoint.condition") + + // K8SServiceEndpointZoneKey is the attribute Key conforming to the + // "k8s.service.endpoint.zone" semantic conventions. It represents the zone of + // the service endpoint. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "us-east-1a", "us-west-2b", "zone-a", "" + // Note: The zone where the endpoint is located, typically corresponding to a + // failure domain. + // This attribute aligns with the `zone` field of endpoints in the + // [K8s EndpointSlice]. + // It enables zone-aware monitoring of service endpoint distribution and + // supports + // features like [Topology Aware Routing]. + // + // If the zone is not populated (e.g., nodes without the + // `topology.kubernetes.io/zone` label), + // the attribute value will be an empty string. + // + // [K8s EndpointSlice]: https://kubernetes.io/docs/reference/kubernetes-api/service-resources/endpoint-slice-v1/ + // [Topology Aware Routing]: https://kubernetes.io/docs/concepts/services-networking/topology-aware-routing/ + K8SServiceEndpointZoneKey = attribute.Key("k8s.service.endpoint.zone") + + // K8SServiceNameKey is the attribute Key conforming to the "k8s.service.name" + // semantic conventions. It represents the name of the Service. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "my-service" + K8SServiceNameKey = attribute.Key("k8s.service.name") + + // K8SServicePublishNotReadyAddressesKey is the attribute Key conforming to the + // "k8s.service.publish_not_ready_addresses" semantic conventions. It represents + // the whether the Service publishes not-ready endpoints. + // + // Type: boolean + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: true, false + // Note: Whether the Service is configured to publish endpoints before the pods + // are ready. + // This attribute is typically used to indicate that a Service (such as a + // headless + // Service for a StatefulSet) allows peer discovery before pods pass their + // readiness probes. + // It aligns with the `publishNotReadyAddresses` field of the + // [K8s ServiceSpec]. + // + // [K8s ServiceSpec]: https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec + K8SServicePublishNotReadyAddressesKey = attribute.Key("k8s.service.publish_not_ready_addresses") + + // K8SServiceTrafficDistributionKey is the attribute Key conforming to the + // "k8s.service.traffic_distribution" semantic conventions. It represents the + // traffic distribution policy for the Service. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "PreferSameZone", "PreferSameNode" + // Note: Specifies how traffic is distributed to endpoints for this Service. + // This attribute aligns with the `trafficDistribution` field of the + // [K8s ServiceSpec]. + // Known values include `PreferSameZone` (prefer endpoints in the same zone as + // the client) and + // `PreferSameNode` (prefer endpoints on the same node, fallback to same zone, + // then cluster-wide). + // If this field is not set on the Service, the attribute SHOULD NOT be emitted. + // When not set, Kubernetes distributes traffic evenly across all endpoints + // cluster-wide. + // + // [K8s ServiceSpec]: https://kubernetes.io/docs/reference/networking/virtual-ips/#traffic-distribution + K8SServiceTrafficDistributionKey = attribute.Key("k8s.service.traffic_distribution") + + // K8SServiceTypeKey is the attribute Key conforming to the "k8s.service.type" + // semantic conventions. It represents the type of the Kubernetes Service. + // + // Type: Enum + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "ClusterIP", "NodePort", "LoadBalancer" + // Note: This attribute aligns with the `type` field of the + // [K8s ServiceSpec]. + // + // [K8s ServiceSpec]: https://kubernetes.io/docs/reference/kubernetes-api/service-resources/service-v1/#ServiceSpec + K8SServiceTypeKey = attribute.Key("k8s.service.type") + + // K8SServiceUIDKey is the attribute Key conforming to the "k8s.service.uid" + // semantic conventions. It represents the UID of the Service. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" + K8SServiceUIDKey = attribute.Key("k8s.service.uid") + // K8SStatefulSetNameKey is the attribute Key conforming to the // "k8s.statefulset.name" semantic conventions. It represents the name of the // StatefulSet. // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "opentelemetry" K8SStatefulSetNameKey = attribute.Key("k8s.statefulset.name") @@ -9400,7 +9720,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Alpha + // Stability: Beta // // Examples: "275ecb36-5aa8-4c2a-9c47-d8bb681b9aff" K8SStatefulSetUIDKey = attribute.Key("k8s.statefulset.uid") @@ -9803,6 +10123,64 @@ func K8SResourceQuotaUID(val string) attribute.KeyValue { return K8SResourceQuotaUIDKey.String(val) } +// K8SServiceAnnotation returns an attribute KeyValue conforming to the +// "k8s.service.annotation" semantic conventions. It represents the annotation +// placed on the Service, the `` being the annotation name, the value being +// the annotation value, even if the value is empty. +func K8SServiceAnnotation(key string, val string) attribute.KeyValue { + return attribute.String("k8s.service.annotation."+key, val) +} + +// K8SServiceEndpointZone returns an attribute KeyValue conforming to the +// "k8s.service.endpoint.zone" semantic conventions. It represents the zone of +// the service endpoint. +func K8SServiceEndpointZone(val string) attribute.KeyValue { + return K8SServiceEndpointZoneKey.String(val) +} + +// K8SServiceLabel returns an attribute KeyValue conforming to the +// "k8s.service.label" semantic conventions. It represents the label placed on +// the Service, the `` being the label name, the value being the label +// value, even if the value is empty. +func K8SServiceLabel(key string, val string) attribute.KeyValue { + return attribute.String("k8s.service.label."+key, val) +} + +// K8SServiceName returns an attribute KeyValue conforming to the +// "k8s.service.name" semantic conventions. It represents the name of the +// Service. +func K8SServiceName(val string) attribute.KeyValue { + return K8SServiceNameKey.String(val) +} + +// K8SServicePublishNotReadyAddresses returns an attribute KeyValue conforming to +// the "k8s.service.publish_not_ready_addresses" semantic conventions. It +// represents the whether the Service publishes not-ready endpoints. +func K8SServicePublishNotReadyAddresses(val bool) attribute.KeyValue { + return K8SServicePublishNotReadyAddressesKey.Bool(val) +} + +// K8SServiceSelector returns an attribute KeyValue conforming to the +// "k8s.service.selector" semantic conventions. It represents the selector +// key-value pair placed on the Service, the `` being the selector key, the +// value being the selector value. +func K8SServiceSelector(key string, val string) attribute.KeyValue { + return attribute.String("k8s.service.selector."+key, val) +} + +// K8SServiceTrafficDistribution returns an attribute KeyValue conforming to the +// "k8s.service.traffic_distribution" semantic conventions. It represents the +// traffic distribution policy for the Service. +func K8SServiceTrafficDistribution(val string) attribute.KeyValue { + return K8SServiceTrafficDistributionKey.String(val) +} + +// K8SServiceUID returns an attribute KeyValue conforming to the +// "k8s.service.uid" semantic conventions. It represents the UID of the Service. +func K8SServiceUID(val string) attribute.KeyValue { + return K8SServiceUIDKey.String(val) +} + // K8SStatefulSetAnnotation returns an attribute KeyValue conforming to the // "k8s.statefulset.annotation" semantic conventions. It represents the // annotation placed on the StatefulSet, the `` being the annotation name, @@ -9995,6 +10373,48 @@ var ( K8SPodStatusReasonUnexpectedAdmissionError = K8SPodStatusReasonKey.String("UnexpectedAdmissionError") ) +// Enum values for k8s.service.endpoint.address_type +var ( + // IPv4 address type + // Stability: development + K8SServiceEndpointAddressTypeIPv4 = K8SServiceEndpointAddressTypeKey.String("IPv4") + // IPv6 address type + // Stability: development + K8SServiceEndpointAddressTypeIPv6 = K8SServiceEndpointAddressTypeKey.String("IPv6") + // FQDN address type + // Stability: development + K8SServiceEndpointAddressTypeFqdn = K8SServiceEndpointAddressTypeKey.String("FQDN") +) + +// Enum values for k8s.service.endpoint.condition +var ( + // The endpoint is ready to receive new connections. + // Stability: development + K8SServiceEndpointConditionReady = K8SServiceEndpointConditionKey.String("ready") + // The endpoint is currently handling traffic. + // Stability: development + K8SServiceEndpointConditionServing = K8SServiceEndpointConditionKey.String("serving") + // The endpoint is in the process of shutting down. + // Stability: development + K8SServiceEndpointConditionTerminating = K8SServiceEndpointConditionKey.String("terminating") +) + +// Enum values for k8s.service.type +var ( + // ClusterIP service type + // Stability: development + K8SServiceTypeClusterIP = K8SServiceTypeKey.String("ClusterIP") + // NodePort service type + // Stability: development + K8SServiceTypeNodePort = K8SServiceTypeKey.String("NodePort") + // LoadBalancer service type + // Stability: development + K8SServiceTypeLoadBalancer = K8SServiceTypeKey.String("LoadBalancer") + // ExternalName service type + // Stability: development + K8SServiceTypeExternalName = K8SServiceTypeKey.String("ExternalName") +) + // Enum values for k8s.volume.type var ( // A [persistentVolumeClaim] volume @@ -11770,6 +12190,16 @@ func OncRPCVersion(val int) attribute.KeyValue { // Namespace: openai const ( + // OpenAIAPITypeKey is the attribute Key conforming to the "openai.api.type" + // semantic conventions. It represents the type of OpenAI API being used. + // + // Type: Enum + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: + OpenAIAPITypeKey = attribute.Key("openai.api.type") + // OpenAIRequestServiceTierKey is the attribute Key conforming to the // "openai.request.service_tier" semantic conventions. It represents the service // tier requested. May be a specific tier, default, or auto. @@ -11818,6 +12248,20 @@ func OpenAIResponseSystemFingerprint(val string) attribute.KeyValue { return OpenAIResponseSystemFingerprintKey.String(val) } +// Enum values for openai.api.type +var ( + // The OpenAI [Chat Completions API]. + // Stability: development + // + // [Chat Completions API]: https://developers.openai.com/api/reference/chat-completions/overview + OpenAIAPITypeChatCompletions = OpenAIAPITypeKey.String("chat_completions") + // The OpenAI [Responses API]. + // Stability: development + // + // [Responses API]: https://developers.openai.com/api/reference/responses/overview + OpenAIAPITypeResponses = OpenAIAPITypeKey.String("responses") +) + // Enum values for openai.request.service_tier var ( // The system will utilize scale tier credits until they are exhausted. @@ -11892,6 +12336,158 @@ var ( OpenTracingRefTypeFollowsFrom = OpenTracingRefTypeKey.String("follows_from") ) +// Namespace: oracle +const ( + // OracleDBDomainKey is the attribute Key conforming to the "oracle.db.domain" + // semantic conventions. It represents the database domain associated with the + // connection. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "example.com", "corp.internal", "prod.db.local" + // Note: This attribute SHOULD be set to the value of the `DB_DOMAIN` + // initialization parameter, + // as exposed in `v$parameter`. `DB_DOMAIN` defines the domain portion of the + // global + // database name and SHOULD be configured when a database is, or may become, + // part of a + // distributed environment. Its value consists of one or more valid identifiers + // (alphanumeric ASCII characters) separated by periods. + OracleDBDomainKey = attribute.Key("oracle.db.domain") + + // OracleDBInstanceNameKey is the attribute Key conforming to the + // "oracle.db.instance.name" semantic conventions. It represents the instance + // name associated with the connection in an Oracle Real Application Clusters + // environment. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "ORCL1", "ORCL2", "ORCL3" + // Note: There can be multiple instances associated with a single database + // service. It indicates the + // unique instance name to which the connection is currently bound. For non-RAC + // databases, this value + // defaults to the `oracle.db.name`. + OracleDBInstanceNameKey = attribute.Key("oracle.db.instance.name") + + // OracleDBNameKey is the attribute Key conforming to the "oracle.db.name" + // semantic conventions. It represents the database name associated with the + // connection. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "ORCL1", "FREE" + // Note: This attribute SHOULD be set to the value of the parameter `DB_NAME` + // exposed in `v$parameter`. + OracleDBNameKey = attribute.Key("oracle.db.name") + + // OracleDBPdbKey is the attribute Key conforming to the "oracle.db.pdb" + // semantic conventions. It represents the pluggable database (PDB) name + // associated with the connection. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "PDB1", "FREEPDB" + // Note: This attribute SHOULD reflect the PDB that the session is currently + // connected to. + // If instrumentation cannot reliably obtain the active PDB name for each + // operation + // without issuing an additional query (such as `SELECT SYS_CONTEXT`), it is + // RECOMMENDED to fall back to the PDB name specified at connection + // establishment. + OracleDBPdbKey = attribute.Key("oracle.db.pdb") + + // OracleDBServiceKey is the attribute Key conforming to the "oracle.db.service" + // semantic conventions. It represents the service name currently associated + // with the database connection. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "order-processing-service", "db_low.adb.oraclecloud.com", + // "db_high.adb.oraclecloud.com" + // Note: The effective service name for a connection can change during its + // lifetime, + // for example after executing sql, `ALTER SESSION`. If an instrumentation + // cannot reliably + // obtain the current service name for each operation without issuing an + // additional + // query (such as `SELECT SYS_CONTEXT`), it is RECOMMENDED to fall back to the + // service name originally provided at connection establishment. + OracleDBServiceKey = attribute.Key("oracle.db.service") +) + +// OracleDBDomain returns an attribute KeyValue conforming to the +// "oracle.db.domain" semantic conventions. It represents the database domain +// associated with the connection. +func OracleDBDomain(val string) attribute.KeyValue { + return OracleDBDomainKey.String(val) +} + +// OracleDBInstanceName returns an attribute KeyValue conforming to the +// "oracle.db.instance.name" semantic conventions. It represents the instance +// name associated with the connection in an Oracle Real Application Clusters +// environment. +func OracleDBInstanceName(val string) attribute.KeyValue { + return OracleDBInstanceNameKey.String(val) +} + +// OracleDBName returns an attribute KeyValue conforming to the "oracle.db.name" +// semantic conventions. It represents the database name associated with the +// connection. +func OracleDBName(val string) attribute.KeyValue { + return OracleDBNameKey.String(val) +} + +// OracleDBPdb returns an attribute KeyValue conforming to the "oracle.db.pdb" +// semantic conventions. It represents the pluggable database (PDB) name +// associated with the connection. +func OracleDBPdb(val string) attribute.KeyValue { + return OracleDBPdbKey.String(val) +} + +// OracleDBService returns an attribute KeyValue conforming to the +// "oracle.db.service" semantic conventions. It represents the service name +// currently associated with the database connection. +func OracleDBService(val string) attribute.KeyValue { + return OracleDBServiceKey.String(val) +} + +// Namespace: oracle_cloud +const ( + // OracleCloudRealmKey is the attribute Key conforming to the + // "oracle_cloud.realm" semantic conventions. It represents the OCI realm + // identifier that indicates the isolated partition in which the tenancy and its + // resources reside. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "oc1", "oc2" + // Note: See [OCI documentation on realms] + // + // [OCI documentation on realms]: https://docs.oracle.com/iaas/Content/General/Concepts/regions.htm + OracleCloudRealmKey = attribute.Key("oracle_cloud.realm") +) + +// OracleCloudRealm returns an attribute KeyValue conforming to the +// "oracle_cloud.realm" semantic conventions. It represents the OCI realm +// identifier that indicates the isolated partition in which the tenancy and its +// resources reside. +func OracleCloudRealm(val string) attribute.KeyValue { + return OracleCloudRealmKey.String(val) +} + // Namespace: os const ( // OSBuildIDKey is the attribute Key conforming to the "os.build_id" semantic @@ -12423,6 +13019,33 @@ const ( // // Examples: "/bazinga/" PprofProfileKeepFramesKey = attribute.Key("pprof.profile.keep_frames") + + // PprofScopeDefaultSampleTypeKey is the attribute Key conforming to the + // "pprof.scope.default_sample_type" semantic conventions. It represents the + // records the pprof's default_sample_type in the original profile. Not set if + // the default sample type was missing. + // + // Type: string + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "cpu" + // Note: This attribute, if present, MUST be set at the scope level + // (resource_profiles[].scope_profiles[].scope.attributes[]). + PprofScopeDefaultSampleTypeKey = attribute.Key("pprof.scope.default_sample_type") + + // PprofScopeSampleTypeOrderKey is the attribute Key conforming to the + // "pprof.scope.sample_type_order" semantic conventions. It represents the + // records the indexes of the sample types in the original profile. + // + // Type: int[] + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: 3, 0, 1, 2 + // Note: This attribute, if present, MUST be set at the scope level + // (resource_profiles[].scope_profiles[].scope.attributes[]). + PprofScopeSampleTypeOrderKey = attribute.Key("pprof.scope.sample_type_order") ) // PprofLocationIsFolded returns an attribute KeyValue conforming to the @@ -12494,6 +13117,21 @@ func PprofProfileKeepFrames(val string) attribute.KeyValue { return PprofProfileKeepFramesKey.String(val) } +// PprofScopeDefaultSampleType returns an attribute KeyValue conforming to the +// "pprof.scope.default_sample_type" semantic conventions. It represents the +// records the pprof's default_sample_type in the original profile. Not set if +// the default sample type was missing. +func PprofScopeDefaultSampleType(val string) attribute.KeyValue { + return PprofScopeDefaultSampleTypeKey.String(val) +} + +// PprofScopeSampleTypeOrder returns an attribute KeyValue conforming to the +// "pprof.scope.sample_type_order" semantic conventions. It represents the +// records the indexes of the sample types in the original profile. +func PprofScopeSampleTypeOrder(val ...int) attribute.KeyValue { + return PprofScopeSampleTypeOrderKey.IntSlice(val) +} + // Namespace: process const ( // ProcessArgsCountKey is the attribute Key conforming to the @@ -13258,59 +13896,13 @@ var ( // Namespace: rpc const ( - // RPCMessageCompressedSizeKey is the attribute Key conforming to the - // "rpc.message.compressed_size" semantic conventions. It represents the - // compressed size of the message in bytes. - // - // Type: int - // RequirementLevel: Recommended - // Stability: Development - // - // Examples: - RPCMessageCompressedSizeKey = attribute.Key("rpc.message.compressed_size") - - // RPCMessageIDKey is the attribute Key conforming to the "rpc.message.id" - // semantic conventions. It MUST be calculated as two different counters - // starting from `1` one for sent messages and one for received message.. - // - // Type: int - // RequirementLevel: Recommended - // Stability: Development - // - // Examples: - // Note: This way we guarantee that the values will be consistent between - // different implementations. - RPCMessageIDKey = attribute.Key("rpc.message.id") - - // RPCMessageTypeKey is the attribute Key conforming to the "rpc.message.type" - // semantic conventions. It represents the whether this is a received or sent - // message. - // - // Type: Enum - // RequirementLevel: Recommended - // Stability: Development - // - // Examples: - RPCMessageTypeKey = attribute.Key("rpc.message.type") - - // RPCMessageUncompressedSizeKey is the attribute Key conforming to the - // "rpc.message.uncompressed_size" semantic conventions. It represents the - // uncompressed size of the message in bytes. - // - // Type: int - // RequirementLevel: Recommended - // Stability: Development - // - // Examples: - RPCMessageUncompressedSizeKey = attribute.Key("rpc.message.uncompressed_size") - // RPCMethodKey is the attribute Key conforming to the "rpc.method" semantic // conventions. It represents the fully-qualified logical name of the method // from the RPC interface perspective. // // Type: string // RequirementLevel: Recommended - // Stability: Development + // Stability: Release_Candidate // // Examples: "com.example.ExampleService/exampleMethod", "EchoService/Echo", // "_OTHER" @@ -13345,7 +13937,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Development + // Stability: Release_Candidate // // Examples: "com.myservice.EchoService/catchAll", // "com.myservice.EchoService/unknownMethod", "InvalidMethod" @@ -13357,7 +13949,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Development + // Stability: Release_Candidate // // Examples: "OK", "DEADLINE_EXCEEDED", "-32602" // Note: Usually it represents an error code, but may also represent partial @@ -13373,7 +13965,7 @@ const ( // // Type: Enum // RequirementLevel: Recommended - // Stability: Development + // Stability: Release_Candidate // // Examples: // Note: The client and server RPC systems may differ for the same RPC @@ -13383,27 +13975,6 @@ const ( RPCSystemNameKey = attribute.Key("rpc.system.name") ) -// RPCMessageCompressedSize returns an attribute KeyValue conforming to the -// "rpc.message.compressed_size" semantic conventions. It represents the -// compressed size of the message in bytes. -func RPCMessageCompressedSize(val int) attribute.KeyValue { - return RPCMessageCompressedSizeKey.Int(val) -} - -// RPCMessageID returns an attribute KeyValue conforming to the "rpc.message.id" -// semantic conventions. It MUST be calculated as two different counters starting -// from `1` one for sent messages and one for received message.. -func RPCMessageID(val int) attribute.KeyValue { - return RPCMessageIDKey.Int(val) -} - -// RPCMessageUncompressedSize returns an attribute KeyValue conforming to the -// "rpc.message.uncompressed_size" semantic conventions. It represents the -// uncompressed size of the message in bytes. -func RPCMessageUncompressedSize(val int) attribute.KeyValue { - return RPCMessageUncompressedSizeKey.Int(val) -} - // RPCMethod returns an attribute KeyValue conforming to the "rpc.method" // semantic conventions. It represents the fully-qualified logical name of the // method from the RPC interface perspective. @@ -13441,25 +14012,15 @@ func RPCResponseStatusCode(val string) attribute.KeyValue { return RPCResponseStatusCodeKey.String(val) } -// Enum values for rpc.message.type -var ( - // sent - // Stability: development - RPCMessageTypeSent = RPCMessageTypeKey.String("SENT") - // received - // Stability: development - RPCMessageTypeReceived = RPCMessageTypeKey.String("RECEIVED") -) - // Enum values for rpc.system.name var ( // [gRPC] - // Stability: development + // Stability: release_candidate // // [gRPC]: https://grpc.io/ RPCSystemNameGRPC = RPCSystemNameKey.String("grpc") // [Apache Dubbo] - // Stability: development + // Stability: release_candidate // // [Apache Dubbo]: https://dubbo.apache.org/ RPCSystemNameDubbo = RPCSystemNameKey.String("dubbo") @@ -13674,13 +14235,28 @@ func ServerPort(val int) attribute.KeyValue { // Namespace: service const ( + // ServiceCriticalityKey is the attribute Key conforming to the + // "service.criticality" semantic conventions. It represents the operational + // criticality of the service. + // + // Type: Enum + // RequirementLevel: Recommended + // Stability: Development + // + // Examples: "critical", "high", "medium", "low" + // Note: Application developers are encouraged to set `service.criticality` to + // express the operational importance of their services. Telemetry consumers MAY + // use this attribute to optimize telemetry collection or improve user + // experience. + ServiceCriticalityKey = attribute.Key("service.criticality") + // ServiceInstanceIDKey is the attribute Key conforming to the // "service.instance.id" semantic conventions. It represents the string ID of // the service instance. // // Type: string // RequirementLevel: Recommended - // Stability: Development + // Stability: Stable // // Examples: "627cc493-f310-47de-96bd-71410b7dec09" // Note: MUST be unique for each instance of the same @@ -13754,7 +14330,7 @@ const ( // // Type: string // RequirementLevel: Recommended - // Stability: Development + // Stability: Stable // // Examples: "Shop" // Note: A string value having a meaning that helps to distinguish a group of @@ -13856,6 +14432,29 @@ func ServiceVersion(val string) attribute.KeyValue { return ServiceVersionKey.String(val) } +// Enum values for service.criticality +var ( + // Service is business-critical; downtime directly impacts revenue, user + // experience, or core functionality. + // + // Stability: development + ServiceCriticalityCritical = ServiceCriticalityKey.String("critical") + // Service is important but has degradation tolerance or fallback mechanisms. + // + // Stability: development + ServiceCriticalityHigh = ServiceCriticalityKey.String("high") + // Service provides supplementary functionality; degradation has limited user + // impact. + // + // Stability: development + ServiceCriticalityMedium = ServiceCriticalityKey.String("medium") + // Service is non-essential to core operations; used for background tasks or + // internal tools. + // + // Stability: development + ServiceCriticalityLow = ServiceCriticalityKey.String("low") +) + // Namespace: session const ( // SessionIDKey is the attribute Key conforming to the "session.id" semantic @@ -15175,6 +15774,18 @@ const ( // // This list is subject to change over time. // + // Matching of query parameter keys against the sensitive list SHOULD be + // case-sensitive. + // + // + // Instrumentation MAY provide a way to override this list via declarative + // configuration. + // If so, it SHOULD use the `sensitive_query_parameters` property + // (an array of case-sensitive strings with minimum items 0) under + // `.instrumentation/development.general.sanitization.url`. + // This list is a full override of the default sensitive query parameter keys, + // it is not a list of keys in addition to the defaults. + // // When a query string value is redacted, the query string key SHOULD still be // preserved, e.g. // `https://www.example.com/path?color=blue&sig=REDACTED`. @@ -15250,6 +15861,17 @@ const ( // // This list is subject to change over time. // + // Matching of query parameter keys against the sensitive list SHOULD be + // case-sensitive. + // + // Instrumentation MAY provide a way to override this list via declarative + // configuration. + // If so, it SHOULD use the `sensitive_query_parameters` property + // (an array of case-sensitive strings with minimum items 0) under + // `.instrumentation/development.general.sanitization.url`. + // This list is a full override of the default sensitive query parameter keys, + // it is not a list of keys in addition to the defaults. + // // When a query string value is redacted, the query string key SHOULD still be // preserved, e.g. // `q=OpenTelemetry&sig=REDACTED`. diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/doc.go b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/doc.go similarity index 80% rename from vendor/go.opentelemetry.io/otel/semconv/v1.39.0/doc.go rename to vendor/go.opentelemetry.io/otel/semconv/v1.40.0/doc.go index 852362ef..c5c41e4d 100644 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/doc.go +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/doc.go @@ -4,6 +4,6 @@ // Package semconv implements OpenTelemetry semantic conventions. // // OpenTelemetry semantic conventions are agreed standardized naming -// patterns for OpenTelemetry things. This package represents the v1.39.0 +// patterns for OpenTelemetry things. This package represents the v1.40.0 // version of the OpenTelemetry semantic conventions. -package semconv // import "go.opentelemetry.io/otel/semconv/v1.39.0" +package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0" diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/error_type.go b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/error_type.go similarity index 62% rename from vendor/go.opentelemetry.io/otel/semconv/v1.39.0/error_type.go rename to vendor/go.opentelemetry.io/otel/semconv/v1.40.0/error_type.go index 84cf636a..6d26e528 100644 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/error_type.go +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/error_type.go @@ -1,9 +1,10 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package semconv // import "go.opentelemetry.io/otel/semconv/v1.39.0" +package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0" import ( + "errors" "reflect" "go.opentelemetry.io/otel/attribute" @@ -14,12 +15,14 @@ import ( // If err is nil, the returned attribute has the default value // [ErrorTypeOther]. // -// If err's type has the method +// If err or one of the errors in its chain has the method // // ErrorType() string // -// then the returned attribute has the value of err.ErrorType(). Otherwise, the -// returned attribute has a value derived from the concrete type of err. +// the returned attribute has that method's return value. If multiple errors in +// the chain implement this method, the value from the first match found by +// [errors.As] is used. Otherwise, the returned attribute has a value derived +// from the concrete type of err. // // The key of the returned attribute is [ErrorTypeKey]. func ErrorType(err error) attribute.KeyValue { @@ -33,8 +36,15 @@ func ErrorType(err error) attribute.KeyValue { func errorType(err error) string { var s string if et, ok := err.(interface{ ErrorType() string }); ok { - // Prioritize the ErrorType method if available. + // Fast path: check the top-level error first. s = et.ErrorType() + } else { + // Fallback: search the error chain for an ErrorType method. + var et interface{ ErrorType() string } + if errors.As(err, &et) { + // Prioritize the ErrorType method if available. + s = et.ErrorType() + } } if s == "" { // Fallback to reflection if the ErrorType method is not supported or diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/exception.go b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/exception.go similarity index 74% rename from vendor/go.opentelemetry.io/otel/semconv/v1.39.0/exception.go rename to vendor/go.opentelemetry.io/otel/semconv/v1.40.0/exception.go index 7b688ecc..6a26231a 100644 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/exception.go +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/exception.go @@ -1,7 +1,7 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package semconv // import "go.opentelemetry.io/otel/semconv/v1.39.0" +package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0" const ( // ExceptionEventName is the name of the Span event representing an exception. diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/otelconv/metric.go b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/otelconv/metric.go similarity index 100% rename from vendor/go.opentelemetry.io/otel/semconv/v1.39.0/otelconv/metric.go rename to vendor/go.opentelemetry.io/otel/semconv/v1.40.0/otelconv/metric.go diff --git a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/schema.go b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/schema.go similarity index 71% rename from vendor/go.opentelemetry.io/otel/semconv/v1.39.0/schema.go rename to vendor/go.opentelemetry.io/otel/semconv/v1.40.0/schema.go index e1a199d8..a07ffa33 100644 --- a/vendor/go.opentelemetry.io/otel/semconv/v1.39.0/schema.go +++ b/vendor/go.opentelemetry.io/otel/semconv/v1.40.0/schema.go @@ -1,9 +1,9 @@ // Copyright The OpenTelemetry Authors // SPDX-License-Identifier: Apache-2.0 -package semconv // import "go.opentelemetry.io/otel/semconv/v1.39.0" +package semconv // import "go.opentelemetry.io/otel/semconv/v1.40.0" // SchemaURL is the schema URL that matches the version of the semantic conventions // that this package defines. Semconv packages starting from v1.4.0 must declare // non-empty schema URL in the form https://opentelemetry.io/schemas/ -const SchemaURL = "https://opentelemetry.io/schemas/1.39.0" +const SchemaURL = "https://opentelemetry.io/schemas/1.40.0" diff --git a/vendor/go.opentelemetry.io/otel/trace/auto.go b/vendor/go.opentelemetry.io/otel/trace/auto.go index 604fdab4..9316fd0a 100644 --- a/vendor/go.opentelemetry.io/otel/trace/auto.go +++ b/vendor/go.opentelemetry.io/otel/trace/auto.go @@ -20,7 +20,7 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" - semconv "go.opentelemetry.io/otel/semconv/v1.39.0" + semconv "go.opentelemetry.io/otel/semconv/v1.40.0" "go.opentelemetry.io/otel/trace/embedded" "go.opentelemetry.io/otel/trace/internal/telemetry" ) diff --git a/vendor/go.opentelemetry.io/otel/trace/trace.go b/vendor/go.opentelemetry.io/otel/trace/trace.go index ee6f4bcb..e3d103c4 100644 --- a/vendor/go.opentelemetry.io/otel/trace/trace.go +++ b/vendor/go.opentelemetry.io/otel/trace/trace.go @@ -12,6 +12,11 @@ const ( // with the sampling bit set means the span is sampled. FlagsSampled = TraceFlags(0x01) + // FlagsRandom is a bitmask with the random trace ID flag set. When + // set, it signals that the trace ID was generated randomly with at + // least 56 bits of randomness (W3C Trace Context Level 2). + FlagsRandom = TraceFlags(0x02) + errInvalidHexID errorConst = "trace-id and span-id can only contain [0-9a-f] characters, all lowercase" errInvalidTraceIDLength errorConst = "hex encoded trace-id must have length equals to 32" @@ -191,6 +196,20 @@ func (tf TraceFlags) WithSampled(sampled bool) TraceFlags { // nolint:revive // return tf &^ FlagsSampled } +// IsRandom reports whether the random bit is set in the TraceFlags. +func (tf TraceFlags) IsRandom() bool { + return tf&FlagsRandom == FlagsRandom +} + +// WithRandom sets the random bit in a new copy of the TraceFlags. +func (tf TraceFlags) WithRandom(random bool) TraceFlags { // nolint:revive // random is not a control flag. + if random { + return tf | FlagsRandom + } + + return tf &^ FlagsRandom +} + // MarshalJSON implements a custom marshal function to encode TraceFlags // as a hex string. func (tf TraceFlags) MarshalJSON() ([]byte, error) { @@ -317,6 +336,11 @@ func (sc SpanContext) IsSampled() bool { return sc.traceFlags.IsSampled() } +// IsRandom reports whether the random bit is set in the SpanContext's TraceFlags. +func (sc SpanContext) IsRandom() bool { + return sc.traceFlags.IsRandom() +} + // WithTraceFlags returns a new SpanContext with the TraceFlags replaced. func (sc SpanContext) WithTraceFlags(flags TraceFlags) SpanContext { return SpanContext{ diff --git a/vendor/go.opentelemetry.io/otel/trace/tracestate.go b/vendor/go.opentelemetry.io/otel/trace/tracestate.go index df65c5cc..e9cb3fd4 100644 --- a/vendor/go.opentelemetry.io/otel/trace/tracestate.go +++ b/vendor/go.opentelemetry.io/otel/trace/tracestate.go @@ -64,7 +64,7 @@ func checkKeyRemain(key string) bool { if v > 127 { return false } - if isAlphaNum(byte(v)) { + if isAlphaNumASCII(v) { continue } switch v { @@ -92,7 +92,7 @@ func checkKeyPart(key string, n int) bool { return ret && checkKeyRemain(key[1:]) } -func isAlphaNum(c byte) bool { +func isAlphaNumASCII[T rune | byte](c T) bool { if c >= 'a' && c <= 'z' { return true } @@ -108,7 +108,7 @@ func checkKeyTenant(key string, n int) bool { if key == "" { return false } - return isAlphaNum(key[0]) && len(key[1:]) <= n && checkKeyRemain(key[1:]) + return isAlphaNumASCII(key[0]) && len(key[1:]) <= n && checkKeyRemain(key[1:]) } // based on the W3C Trace Context specification diff --git a/vendor/go.opentelemetry.io/otel/version.go b/vendor/go.opentelemetry.io/otel/version.go index 90045340..1db4f47e 100644 --- a/vendor/go.opentelemetry.io/otel/version.go +++ b/vendor/go.opentelemetry.io/otel/version.go @@ -5,5 +5,5 @@ package otel // import "go.opentelemetry.io/otel" // Version is the current release version of OpenTelemetry in use. func Version() string { - return "1.41.0" + return "1.43.0" } diff --git a/vendor/go.opentelemetry.io/otel/versions.yaml b/vendor/go.opentelemetry.io/otel/versions.yaml index 479c7478..bcc6ee78 100644 --- a/vendor/go.opentelemetry.io/otel/versions.yaml +++ b/vendor/go.opentelemetry.io/otel/versions.yaml @@ -3,7 +3,7 @@ module-sets: stable-v1: - version: v1.41.0 + version: v1.43.0 modules: - go.opentelemetry.io/otel - go.opentelemetry.io/otel/bridge/opencensus @@ -22,11 +22,11 @@ module-sets: - go.opentelemetry.io/otel/sdk/metric - go.opentelemetry.io/otel/trace experimental-metrics: - version: v0.63.0 + version: v0.65.0 modules: - go.opentelemetry.io/otel/exporters/prometheus experimental-logs: - version: v0.17.0 + version: v0.19.0 modules: - go.opentelemetry.io/otel/log - go.opentelemetry.io/otel/log/logtest @@ -36,7 +36,7 @@ module-sets: - go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp - go.opentelemetry.io/otel/exporters/stdout/stdoutlog experimental-schema: - version: v0.0.15 + version: v0.0.16 modules: - go.opentelemetry.io/otel/schema excluded-modules: @@ -64,3 +64,6 @@ modules: go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp: version-refs: - ./internal/version.go + go.opentelemetry.io/otel/exporters/stdout/stdoutlog: + version-refs: + - ./internal/version.go diff --git a/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s new file mode 100644 index 00000000..e07fa75e --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/asm_darwin_arm64_gc.s @@ -0,0 +1,12 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +#include "textflag.h" + +TEXT libc_sysctlbyname_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctlbyname(SB) +GLOBL Β·libc_sysctlbyname_trampoline_addr(SB), RODATA, $8 +DATA Β·libc_sysctlbyname_trampoline_addr(SB)/8, $libc_sysctlbyname_trampoline<>(SB) diff --git a/vendor/golang.org/x/sys/cpu/cpu_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_arm64.go index 6d8eb784..5fc09e29 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_arm64.go @@ -44,14 +44,11 @@ func initOptions() { } func archInit() { - switch runtime.GOOS { - case "freebsd": + if runtime.GOOS == "freebsd" { readARM64Registers() - case "linux", "netbsd", "openbsd", "windows": + } else { + // Most platforms don't seem to allow directly reading these registers. doinit() - default: - // Many platforms don't seem to allow reading these registers. - setMinimalFeatures() } } diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go new file mode 100644 index 00000000..0b470744 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64.go @@ -0,0 +1,67 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && gc + +package cpu + +func doinit() { + setMinimalFeatures() + + // The feature flags are explained in [Instruction Set Detection]. + // There are some differences between MacOS versions: + // + // MacOS 11 and 12 do not have "hw.optional" sysctl values for some of the features. + // + // MacOS 13 changed some of the naming conventions to align with ARM Architecture Reference Manual. + // For example "hw.optional.armv8_2_sha512" became "hw.optional.arm.FEAT_SHA512". + // It currently checks both to stay compatible with MacOS 11 and 12. + // The old names also work with MacOS 13, however it's not clear whether + // they will continue working with future OS releases. + // + // Once MacOS 12 is no longer supported the old names can be removed. + // + // [Instruction Set Detection]: https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics + + // Encryption, hashing and checksum capabilities + + // For the following flags there are no MacOS 11 sysctl flags. + ARM64.HasAES = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_AES\x00")) + ARM64.HasPMULL = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_PMULL\x00")) + ARM64.HasSHA1 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA1\x00")) + ARM64.HasSHA2 = true || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA256\x00")) + + ARM64.HasSHA3 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha3\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA3\x00")) + ARM64.HasSHA512 = darwinSysctlEnabled([]byte("hw.optional.armv8_2_sha512\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SHA512\x00")) + + ARM64.HasCRC32 = darwinSysctlEnabled([]byte("hw.optional.armv8_crc32\x00")) + + // Atomic and memory ordering + ARM64.HasATOMICS = darwinSysctlEnabled([]byte("hw.optional.armv8_1_atomics\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LSE\x00")) + ARM64.HasLRCPC = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_LRCPC\x00")) + + // SIMD and floating point capabilities + ARM64.HasFPHP = darwinSysctlEnabled([]byte("hw.optional.neon_fp16\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FP16\x00")) + ARM64.HasASIMDHP = darwinSysctlEnabled([]byte("hw.optional.neon_hpfp\x00")) || darwinSysctlEnabled([]byte("hw.optional.AdvSIMD_HPFPCvt\x00")) + ARM64.HasASIMDRDM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_RDM\x00")) + ARM64.HasASIMDDP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DotProd\x00")) + ARM64.HasASIMDFHM = darwinSysctlEnabled([]byte("hw.optional.armv8_2_fhm\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FHM\x00")) + ARM64.HasI8MM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_I8MM\x00")) + + ARM64.HasJSCVT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_JSCVT\x00")) + ARM64.HasFCMA = darwinSysctlEnabled([]byte("hw.optional.armv8_3_compnum\x00")) || darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_FCMA\x00")) + + // Miscellaneous + ARM64.HasDCPOP = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DPB\x00")) + ARM64.HasEVTSTRM = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_ECV\x00")) + ARM64.HasDIT = darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_DIT\x00")) + + // Not supported, but added for completeness + ARM64.HasCPUID = false + + ARM64.HasSM3 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM3\x00")) + ARM64.HasSM4 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SM4\x00")) + ARM64.HasSVE = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE\x00")) + ARM64.HasSVE2 = false // darwinSysctlEnabled([]byte("hw.optional.arm.FEAT_SVE2\x00")) +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go new file mode 100644 index 00000000..4ee68e38 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_darwin_arm64_other.go @@ -0,0 +1,29 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build darwin && arm64 && !gc + +package cpu + +func doinit() { + setMinimalFeatures() + + ARM64.HasASIMD = true + ARM64.HasFP = true + + // Go already assumes these to be available because they were on the M1 + // and these are supported on all Apple arm64 chips. + ARM64.HasAES = true + ARM64.HasPMULL = true + ARM64.HasSHA1 = true + ARM64.HasSHA2 = true + + if runtime.GOOS != "ios" { + // Apple A7 processors do not support these, however + // M-series SoCs are at least armv8.4-a + ARM64.HasCRC32 = true // armv8.1 + ARM64.HasATOMICS = true // armv8.2 + ARM64.HasJSCVT = true // armv8.3, if HasFP + } +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go index 7f194678..05913081 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_gccgo_arm64.go @@ -9,3 +9,4 @@ package cpu func getisar0() uint64 { return 0 } func getisar1() uint64 { return 0 } func getpfr0() uint64 { return 0 } +func getzfr0() uint64 { return 0 } diff --git a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go index ff74d7af..6c7c5bfd 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go @@ -2,8 +2,10 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !linux && !netbsd && !openbsd && !windows && arm64 +//go:build !darwin && !linux && !netbsd && !openbsd && !windows && arm64 package cpu -func doinit() {} +func doinit() { + setMinimalFeatures() +} diff --git a/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go b/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go new file mode 100644 index 00000000..7b4e67ff --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/syscall_darwin_arm64_gc.go @@ -0,0 +1,54 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Minimal copy from internal/cpu and runtime to make sysctl calls. + +//go:build darwin && arm64 && gc + +package cpu + +import ( + "syscall" + "unsafe" +) + +type Errno = syscall.Errno + +// adapted from internal/cpu/cpu_arm64_darwin.go +func darwinSysctlEnabled(name []byte) bool { + out := int32(0) + nout := unsafe.Sizeof(out) + if ret := sysctlbyname(&name[0], (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); ret != nil { + return false + } + return out > 0 +} + +//go:cgo_import_dynamic libc_sysctl sysctl "/usr/lib/libSystem.B.dylib" + +var libc_sysctlbyname_trampoline_addr uintptr + +// adapted from runtime/sys_darwin.go in the pattern of sysctl() above, as defined in x/sys/unix +func sysctlbyname(name *byte, old *byte, oldlen *uintptr, new *byte, newlen uintptr) error { + if _, _, err := syscall_syscall6( + libc_sysctlbyname_trampoline_addr, + uintptr(unsafe.Pointer(name)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen), + 0, + ); err != 0 { + return err + } + + return nil +} + +//go:cgo_import_dynamic libc_sysctlbyname sysctlbyname "/usr/lib/libSystem.B.dylib" + +// Implemented in the runtime package (runtime/sys_darwin.go) +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno) + +//go:linkname syscall_syscall6 syscall.syscall6 diff --git a/vendor/golang.org/x/sys/plan9/syscall_plan9.go b/vendor/golang.org/x/sys/plan9/syscall_plan9.go index d079d811..76191223 100644 --- a/vendor/golang.org/x/sys/plan9/syscall_plan9.go +++ b/vendor/golang.org/x/sys/plan9/syscall_plan9.go @@ -19,13 +19,7 @@ import ( // A Note is a string describing a process note. // It implements the os.Signal interface. -type Note string - -func (n Note) Signal() {} - -func (n Note) String() string { - return string(n) -} +type Note = syscall.Note var ( Stdin = 0 diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index c1a46701..45476a73 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -593,110 +593,115 @@ const ( ) const ( - NDA_UNSPEC = 0x0 - NDA_DST = 0x1 - NDA_LLADDR = 0x2 - NDA_CACHEINFO = 0x3 - NDA_PROBES = 0x4 - NDA_VLAN = 0x5 - NDA_PORT = 0x6 - NDA_VNI = 0x7 - NDA_IFINDEX = 0x8 - NDA_MASTER = 0x9 - NDA_LINK_NETNSID = 0xa - NDA_SRC_VNI = 0xb - NTF_USE = 0x1 - NTF_SELF = 0x2 - NTF_MASTER = 0x4 - NTF_PROXY = 0x8 - NTF_EXT_LEARNED = 0x10 - NTF_OFFLOADED = 0x20 - NTF_ROUTER = 0x80 - NUD_INCOMPLETE = 0x1 - NUD_REACHABLE = 0x2 - NUD_STALE = 0x4 - NUD_DELAY = 0x8 - NUD_PROBE = 0x10 - NUD_FAILED = 0x20 - NUD_NOARP = 0x40 - NUD_PERMANENT = 0x80 - NUD_NONE = 0x0 - IFA_UNSPEC = 0x0 - IFA_ADDRESS = 0x1 - IFA_LOCAL = 0x2 - IFA_LABEL = 0x3 - IFA_BROADCAST = 0x4 - IFA_ANYCAST = 0x5 - IFA_CACHEINFO = 0x6 - IFA_MULTICAST = 0x7 - IFA_FLAGS = 0x8 - IFA_RT_PRIORITY = 0x9 - IFA_TARGET_NETNSID = 0xa - IFAL_LABEL = 0x2 - IFAL_ADDRESS = 0x1 - RT_SCOPE_UNIVERSE = 0x0 - RT_SCOPE_SITE = 0xc8 - RT_SCOPE_LINK = 0xfd - RT_SCOPE_HOST = 0xfe - RT_SCOPE_NOWHERE = 0xff - RT_TABLE_UNSPEC = 0x0 - RT_TABLE_COMPAT = 0xfc - RT_TABLE_DEFAULT = 0xfd - RT_TABLE_MAIN = 0xfe - RT_TABLE_LOCAL = 0xff - RT_TABLE_MAX = 0xffffffff - RTA_UNSPEC = 0x0 - RTA_DST = 0x1 - RTA_SRC = 0x2 - RTA_IIF = 0x3 - RTA_OIF = 0x4 - RTA_GATEWAY = 0x5 - RTA_PRIORITY = 0x6 - RTA_PREFSRC = 0x7 - RTA_METRICS = 0x8 - RTA_MULTIPATH = 0x9 - RTA_FLOW = 0xb - RTA_CACHEINFO = 0xc - RTA_TABLE = 0xf - RTA_MARK = 0x10 - RTA_MFC_STATS = 0x11 - RTA_VIA = 0x12 - RTA_NEWDST = 0x13 - RTA_PREF = 0x14 - RTA_ENCAP_TYPE = 0x15 - RTA_ENCAP = 0x16 - RTA_EXPIRES = 0x17 - RTA_PAD = 0x18 - RTA_UID = 0x19 - RTA_TTL_PROPAGATE = 0x1a - RTA_IP_PROTO = 0x1b - RTA_SPORT = 0x1c - RTA_DPORT = 0x1d - RTN_UNSPEC = 0x0 - RTN_UNICAST = 0x1 - RTN_LOCAL = 0x2 - RTN_BROADCAST = 0x3 - RTN_ANYCAST = 0x4 - RTN_MULTICAST = 0x5 - RTN_BLACKHOLE = 0x6 - RTN_UNREACHABLE = 0x7 - RTN_PROHIBIT = 0x8 - RTN_THROW = 0x9 - RTN_NAT = 0xa - RTN_XRESOLVE = 0xb - SizeofNlMsghdr = 0x10 - SizeofNlMsgerr = 0x14 - SizeofRtGenmsg = 0x1 - SizeofNlAttr = 0x4 - SizeofRtAttr = 0x4 - SizeofIfInfomsg = 0x10 - SizeofIfAddrmsg = 0x8 - SizeofIfAddrlblmsg = 0xc - SizeofIfaCacheinfo = 0x10 - SizeofRtMsg = 0xc - SizeofRtNexthop = 0x8 - SizeofNdUseroptmsg = 0x10 - SizeofNdMsg = 0xc + NDA_UNSPEC = 0x0 + NDA_DST = 0x1 + NDA_LLADDR = 0x2 + NDA_CACHEINFO = 0x3 + NDA_PROBES = 0x4 + NDA_VLAN = 0x5 + NDA_PORT = 0x6 + NDA_VNI = 0x7 + NDA_IFINDEX = 0x8 + NDA_MASTER = 0x9 + NDA_LINK_NETNSID = 0xa + NDA_SRC_VNI = 0xb + NTF_USE = 0x1 + NTF_SELF = 0x2 + NTF_MASTER = 0x4 + NTF_PROXY = 0x8 + NTF_EXT_LEARNED = 0x10 + NTF_OFFLOADED = 0x20 + NTF_ROUTER = 0x80 + NUD_INCOMPLETE = 0x1 + NUD_REACHABLE = 0x2 + NUD_STALE = 0x4 + NUD_DELAY = 0x8 + NUD_PROBE = 0x10 + NUD_FAILED = 0x20 + NUD_NOARP = 0x40 + NUD_PERMANENT = 0x80 + NUD_NONE = 0x0 + IFA_UNSPEC = 0x0 + IFA_ADDRESS = 0x1 + IFA_LOCAL = 0x2 + IFA_LABEL = 0x3 + IFA_BROADCAST = 0x4 + IFA_ANYCAST = 0x5 + IFA_CACHEINFO = 0x6 + IFA_MULTICAST = 0x7 + IFA_FLAGS = 0x8 + IFA_RT_PRIORITY = 0x9 + IFA_TARGET_NETNSID = 0xa + IFAL_LABEL = 0x2 + IFAL_ADDRESS = 0x1 + RT_SCOPE_UNIVERSE = 0x0 + RT_SCOPE_SITE = 0xc8 + RT_SCOPE_LINK = 0xfd + RT_SCOPE_HOST = 0xfe + RT_SCOPE_NOWHERE = 0xff + RT_TABLE_UNSPEC = 0x0 + RT_TABLE_COMPAT = 0xfc + RT_TABLE_DEFAULT = 0xfd + RT_TABLE_MAIN = 0xfe + RT_TABLE_LOCAL = 0xff + RT_TABLE_MAX = 0xffffffff + RTA_UNSPEC = 0x0 + RTA_DST = 0x1 + RTA_SRC = 0x2 + RTA_IIF = 0x3 + RTA_OIF = 0x4 + RTA_GATEWAY = 0x5 + RTA_PRIORITY = 0x6 + RTA_PREFSRC = 0x7 + RTA_METRICS = 0x8 + RTA_MULTIPATH = 0x9 + RTA_FLOW = 0xb + RTA_CACHEINFO = 0xc + RTA_TABLE = 0xf + RTA_MARK = 0x10 + RTA_MFC_STATS = 0x11 + RTA_VIA = 0x12 + RTA_NEWDST = 0x13 + RTA_PREF = 0x14 + RTA_ENCAP_TYPE = 0x15 + RTA_ENCAP = 0x16 + RTA_EXPIRES = 0x17 + RTA_PAD = 0x18 + RTA_UID = 0x19 + RTA_TTL_PROPAGATE = 0x1a + RTA_IP_PROTO = 0x1b + RTA_SPORT = 0x1c + RTA_DPORT = 0x1d + RTN_UNSPEC = 0x0 + RTN_UNICAST = 0x1 + RTN_LOCAL = 0x2 + RTN_BROADCAST = 0x3 + RTN_ANYCAST = 0x4 + RTN_MULTICAST = 0x5 + RTN_BLACKHOLE = 0x6 + RTN_UNREACHABLE = 0x7 + RTN_PROHIBIT = 0x8 + RTN_THROW = 0x9 + RTN_NAT = 0xa + RTN_XRESOLVE = 0xb + PREFIX_UNSPEC = 0x0 + PREFIX_ADDRESS = 0x1 + PREFIX_CACHEINFO = 0x2 + SizeofNlMsghdr = 0x10 + SizeofNlMsgerr = 0x14 + SizeofRtGenmsg = 0x1 + SizeofNlAttr = 0x4 + SizeofRtAttr = 0x4 + SizeofIfInfomsg = 0x10 + SizeofPrefixmsg = 0xc + SizeofPrefixCacheinfo = 0x8 + SizeofIfAddrmsg = 0x8 + SizeofIfAddrlblmsg = 0xc + SizeofIfaCacheinfo = 0x10 + SizeofRtMsg = 0xc + SizeofRtNexthop = 0x8 + SizeofNdUseroptmsg = 0x10 + SizeofNdMsg = 0xc ) type NlMsghdr struct { @@ -735,6 +740,22 @@ type IfInfomsg struct { Change uint32 } +type Prefixmsg struct { + Family uint8 + Pad1 uint8 + Pad2 uint16 + Ifindex int32 + Type uint8 + Len uint8 + Flags uint8 + Pad3 uint8 +} + +type PrefixCacheinfo struct { + Preferred_time uint32 + Valid_time uint32 +} + type IfAddrmsg struct { Family uint8 Prefixlen uint8 diff --git a/vendor/golang.org/x/sys/windows/aliases.go b/vendor/golang.org/x/sys/windows/aliases.go index 16f90560..96317966 100644 --- a/vendor/golang.org/x/sys/windows/aliases.go +++ b/vendor/golang.org/x/sys/windows/aliases.go @@ -8,5 +8,6 @@ package windows import "syscall" +type Signal = syscall.Signal type Errno = syscall.Errno type SysProcAttr = syscall.SysProcAttr diff --git a/vendor/golang.org/x/sys/windows/registry/key.go b/vendor/golang.org/x/sys/windows/registry/key.go index 39aeeb64..7cc6ff3a 100644 --- a/vendor/golang.org/x/sys/windows/registry/key.go +++ b/vendor/golang.org/x/sys/windows/registry/key.go @@ -198,7 +198,20 @@ type KeyInfo struct { // ModTime returns the key's last write time. func (ki *KeyInfo) ModTime() time.Time { - return time.Unix(0, ki.lastWriteTime.Nanoseconds()) + lastHigh, lastLow := ki.lastWriteTime.HighDateTime, ki.lastWriteTime.LowDateTime + // 100-nanosecond intervals since January 1, 1601 + hsec := uint64(lastHigh)<<32 + uint64(lastLow) + // Convert _before_ gauging; the nanosecond difference between Epoch (00:00:00 + // UTC, January 1, 1970) and Filetime's zero offset (January 1, 1601) is out + // of bounds for int64: -11644473600*1e7*1e2 < math.MinInt64 + sec := int64(hsec/1e7) - 11644473600 + nsec := int64(hsec%1e7) * 100 + return time.Unix(sec, nsec) +} + +// modTimeZero reports whether the key's last write time is zero. +func (ki *KeyInfo) modTimeZero() bool { + return ki.lastWriteTime.LowDateTime == 0 && ki.lastWriteTime.HighDateTime == 0 } // Stat retrieves information about the open key k. diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 738a9f21..d7664365 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -1490,20 +1490,6 @@ func Getgid() (gid int) { return -1 } func Getegid() (egid int) { return -1 } func Getgroups() (gids []int, err error) { return nil, syscall.EWINDOWS } -type Signal int - -func (s Signal) Signal() {} - -func (s Signal) String() string { - if 0 <= s && int(s) < len(signals) { - str := signals[s] - if str != "" { - return str - } - } - return "signal " + itoa(int(s)) -} - func LoadCreateSymbolicLink() error { return procCreateSymbolicLinkW.Find() } diff --git a/vendor/modules.txt b/vendor/modules.txt index dcddf3b1..0e530aa0 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -97,9 +97,6 @@ github.com/cloudwego/base64x/internal/rt # github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc ## explicit github.com/davecgh/go-spew/spew -# github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f -## explicit -github.com/dgryski/go-rendezvous # github.com/emicklei/go-restful/v3 v3.12.2 ## explicit; go 1.13 github.com/emicklei/go-restful/v3 @@ -442,14 +439,14 @@ github.com/quic-go/quic-go/quicvarint # github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 ## explicit github.com/rcrowley/go-metrics -# github.com/redis/go-redis/extra/rediscmd/v9 v9.18.0 -## explicit; go 1.21 +# github.com/redis/go-redis/extra/rediscmd/v9 v9.20.1 +## explicit; go 1.24 github.com/redis/go-redis/extra/rediscmd/v9 -# github.com/redis/go-redis/extra/redisotel/v9 v9.18.0 -## explicit; go 1.21 +# github.com/redis/go-redis/extra/redisotel/v9 v9.20.1 +## explicit; go 1.25.0 github.com/redis/go-redis/extra/redisotel/v9 -# github.com/redis/go-redis/v9 v9.18.0 -## explicit; go 1.21 +# github.com/redis/go-redis/v9 v9.20.1 +## explicit; go 1.24 github.com/redis/go-redis/v9 github.com/redis/go-redis/v9/auth github.com/redis/go-redis/v9/internal @@ -461,7 +458,6 @@ github.com/redis/go-redis/v9/internal/maintnotifications/logs github.com/redis/go-redis/v9/internal/otel github.com/redis/go-redis/v9/internal/pool github.com/redis/go-redis/v9/internal/proto -github.com/redis/go-redis/v9/internal/rand github.com/redis/go-redis/v9/internal/routing github.com/redis/go-redis/v9/internal/util github.com/redis/go-redis/v9/maintnotifications @@ -562,8 +558,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconvutil -# go.opentelemetry.io/otel v1.41.0 -## explicit; go 1.24.0 +# go.opentelemetry.io/otel v1.43.0 +## explicit; go 1.25.0 go.opentelemetry.io/otel go.opentelemetry.io/otel/attribute go.opentelemetry.io/otel/attribute/internal @@ -579,8 +575,8 @@ go.opentelemetry.io/otel/semconv/v1.20.0 go.opentelemetry.io/otel/semconv/v1.24.0 go.opentelemetry.io/otel/semconv/v1.26.0 go.opentelemetry.io/otel/semconv/v1.37.0 -go.opentelemetry.io/otel/semconv/v1.39.0 -go.opentelemetry.io/otel/semconv/v1.39.0/otelconv +go.opentelemetry.io/otel/semconv/v1.40.0 +go.opentelemetry.io/otel/semconv/v1.40.0/otelconv # go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 ## explicit; go 1.22.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace @@ -592,13 +588,13 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/envconfig go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry -# go.opentelemetry.io/otel/metric v1.41.0 -## explicit; go 1.24.0 +# go.opentelemetry.io/otel/metric v1.43.0 +## explicit; go 1.25.0 go.opentelemetry.io/otel/metric go.opentelemetry.io/otel/metric/embedded go.opentelemetry.io/otel/metric/noop -# go.opentelemetry.io/otel/sdk v1.40.0 -## explicit; go 1.24.0 +# go.opentelemetry.io/otel/sdk v1.43.0 +## explicit; go 1.25.0 go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/sdk/instrumentation go.opentelemetry.io/otel/sdk/internal/x @@ -606,8 +602,8 @@ go.opentelemetry.io/otel/sdk/resource go.opentelemetry.io/otel/sdk/trace go.opentelemetry.io/otel/sdk/trace/internal/env go.opentelemetry.io/otel/sdk/trace/internal/observ -# go.opentelemetry.io/otel/trace v1.41.0 -## explicit; go 1.24.0 +# go.opentelemetry.io/otel/trace v1.43.0 +## explicit; go 1.25.0 go.opentelemetry.io/otel/trace go.opentelemetry.io/otel/trace/embedded go.opentelemetry.io/otel/trace/internal/telemetry @@ -688,8 +684,8 @@ golang.org/x/oauth2/internal ## explicit; go 1.24.0 golang.org/x/sync/errgroup golang.org/x/sync/singleflight -# golang.org/x/sys v0.41.0 -## explicit; go 1.24.0 +# golang.org/x/sys v0.42.0 +## explicit; go 1.25.0 golang.org/x/sys/cpu golang.org/x/sys/plan9 golang.org/x/sys/unix