update go modules
This commit is contained in:
committed by
Umputun
parent
532573fb34
commit
3210de8f7b
@@ -2,8 +2,6 @@ module github.com/umputun/remark42/memory_store
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.6
|
||||
|
||||
require (
|
||||
github.com/go-pkgz/jrpc v0.3.0
|
||||
github.com/go-pkgz/lgr v0.11.1
|
||||
@@ -23,10 +21,10 @@ require (
|
||||
github.com/didip/tollbooth/v7 v7.0.1 // indirect
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f // indirect
|
||||
github.com/dlclark/regexp2 v1.10.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.11 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.12 // indirect
|
||||
github.com/go-chi/render v1.0.3 // indirect
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||
github.com/go-pkgz/rest v1.18.2 // indirect
|
||||
github.com/go-pkgz/rest v1.19.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
@@ -36,8 +34,9 @@ require (
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.8 // indirect
|
||||
golang.org/x/image v0.15.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -26,8 +26,8 @@ github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f/go.mod h1:q9C8
|
||||
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
|
||||
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
|
||||
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
@@ -39,8 +39,8 @@ github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiw
|
||||
github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM=
|
||||
github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU=
|
||||
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
||||
github.com/go-pkgz/rest v1.18.2 h1:eJYj1qlLJvTx86R4o+XmlKHOAGAX42WeG9PZrJud/e0=
|
||||
github.com/go-pkgz/rest v1.18.2/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
|
||||
github.com/go-pkgz/rest v1.19.0 h1:FNMi5QX5dDIkuC+/e0r+CWsTuOTwUiWMRSA16Ou+9+A=
|
||||
github.com/go-pkgz/rest v1.19.0/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@@ -89,8 +89,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -103,8 +103,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -121,8 +121,9 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+12
-14
@@ -2,27 +2,25 @@ module github.com/umputun/remark42/backend
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.6
|
||||
|
||||
require (
|
||||
github.com/Depado/bfchroma/v2 v2.0.0
|
||||
github.com/PuerkitoBio/goquery v1.8.1
|
||||
github.com/alecthomas/chroma/v2 v2.12.0
|
||||
github.com/didip/tollbooth/v7 v7.0.1
|
||||
github.com/didip/tollbooth_chi v0.0.0-20220719025231-d662a7f6928f
|
||||
github.com/go-chi/chi/v5 v5.0.11
|
||||
github.com/go-chi/chi/v5 v5.0.12
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/go-pkgz/auth v1.22.2-0.20240110230548-7a3db006ab6e
|
||||
github.com/go-pkgz/auth v1.22.2-0.20240117071454-f721b8c33b05
|
||||
github.com/go-pkgz/jrpc v0.3.0
|
||||
github.com/go-pkgz/lcw v1.0.2
|
||||
github.com/go-pkgz/lcw v1.1.0
|
||||
github.com/go-pkgz/lgr v0.11.1
|
||||
github.com/go-pkgz/notify v1.1.0
|
||||
github.com/go-pkgz/repeater v1.1.3
|
||||
github.com/go-pkgz/rest v1.18.2
|
||||
github.com/go-pkgz/rest v1.19.0
|
||||
github.com/go-pkgz/syncs v1.3.2
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/feeds v1.1.2
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
github.com/jessevdk/go-flags v1.5.0
|
||||
@@ -34,9 +32,9 @@ require (
|
||||
github.com/stretchr/testify v1.8.4
|
||||
go.etcd.io/bbolt v1.3.8
|
||||
go.uber.org/goleak v1.3.0
|
||||
golang.org/x/crypto v0.18.0
|
||||
golang.org/x/crypto v0.19.0
|
||||
golang.org/x/image v0.15.0
|
||||
golang.org/x/net v0.20.0
|
||||
golang.org/x/net v0.21.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -53,7 +51,6 @@ require (
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2 // indirect
|
||||
github.com/go-pkgz/email v0.5.0 // indirect
|
||||
github.com/go-pkgz/expirable-cache v1.0.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
@@ -62,17 +59,18 @@ require (
|
||||
github.com/hashicorp/golang-lru v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/montanaflynn/stats v0.7.1 // indirect
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/slack-go/slack v0.12.3 // indirect
|
||||
github.com/redis/go-redis/v9 v9.4.0 // indirect
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d // indirect
|
||||
github.com/slack-go/slack v0.12.4 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.2 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
|
||||
go.mongodb.org/mongo-driver v1.13.1 // indirect
|
||||
golang.org/x/oauth2 v0.16.0 // indirect
|
||||
golang.org/x/oauth2 v0.17.0 // indirect
|
||||
golang.org/x/sync v0.6.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
|
||||
+34
-65
@@ -17,8 +17,8 @@ github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk
|
||||
github.com/alecthomas/repr v0.2.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
|
||||
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
|
||||
github.com/alicebob/miniredis/v2 v2.22.0 h1:lIHHiSkEyS1MkKHCHzN+0mWrA4YdbGdimE5iZ2sHSzo=
|
||||
github.com/alicebob/miniredis/v2 v2.22.0/go.mod h1:XNqvJdQJv5mSuVMc0ynneafpnL/zv52acZ6kqeS0t88=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1 h1:7XAt0uUg3DtwEKW5ZAGa+K7FZV2DdKQo5K/6TTnfX8Y=
|
||||
github.com/alicebob/miniredis/v2 v2.31.1/go.mod h1:UB/T2Uztp7MlFSDakaX1sTXUv5CASoprx0wulRT6HBg=
|
||||
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
@@ -26,12 +26,12 @@ github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsVi
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -50,13 +50,12 @@ github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
|
||||
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
|
||||
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
@@ -64,8 +63,8 @@ github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2 h1:CuZhD3lhGuI6aNLyUbRHXsgG2RwGRBOuCBfd4WQKqBQ=
|
||||
github.com/go-oauth2/oauth2/v4 v4.5.2/go.mod h1:wk/2uLImWIa9VVQDgxz99H2GDbhmfi/9/Xr+GvkSUSQ=
|
||||
github.com/go-pkgz/auth v1.22.2-0.20240110230548-7a3db006ab6e h1:p265UhLsZhoyRT2bXbkJE1MSBHoxjTTvbtSY8ZGuWHY=
|
||||
github.com/go-pkgz/auth v1.22.2-0.20240110230548-7a3db006ab6e/go.mod h1:IN1y3BqIHixxr8dm8QnMK3Z3oSzg80kE0XenjedsHGw=
|
||||
github.com/go-pkgz/auth v1.22.2-0.20240117071454-f721b8c33b05 h1:oe9D1WzmKgG27i9PtDhxpDm+2n7KR431XHBGKLaQDLs=
|
||||
github.com/go-pkgz/auth v1.22.2-0.20240117071454-f721b8c33b05/go.mod h1:/ceyBqZEjBsjP8evrd25LnNM2zMNqj0vc5PIKYJmi0U=
|
||||
github.com/go-pkgz/email v0.5.0 h1:fdtMDGJ8NwyBACLR0LYHaCIK/OeUwZHMhH7Q0+oty9U=
|
||||
github.com/go-pkgz/email v0.5.0/go.mod h1:BdxglsQnymzhfdbnncEE72a6DrucZHy6I+42LK2jLEc=
|
||||
github.com/go-pkgz/expirable-cache v0.1.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
@@ -73,8 +72,8 @@ github.com/go-pkgz/expirable-cache v1.0.0 h1:ns5+1hjY8hntGv8bPaQd9Gr7Jyo+Uw5SLyI
|
||||
github.com/go-pkgz/expirable-cache v1.0.0/go.mod h1:GTrEl0X+q0mPNqN6dtcQXksACnzCBQ5k/k1SwXJsZKs=
|
||||
github.com/go-pkgz/jrpc v0.3.0 h1:Fls38KqPsHzvp0FWfivr6cGnncC+iFBodHBqvUPY+0U=
|
||||
github.com/go-pkgz/jrpc v0.3.0/go.mod h1:MFtKs75JESiSqVicsQkgN2iDFFuCd3gVT1/vKiwRi00=
|
||||
github.com/go-pkgz/lcw v1.0.2 h1:68T8xRkVWSo1KIbMfFXBY1xbFF3XWl7YD+ub3jkUlNE=
|
||||
github.com/go-pkgz/lcw v1.0.2/go.mod h1:CPJJzunpmGToOtD0Ga82TV152eL69sYEIIPcy9fbxlU=
|
||||
github.com/go-pkgz/lcw v1.1.0 h1:hDJdQJZf4iw19a7cTgQvF6Poz/L7mL4E7FgG5jGs4lA=
|
||||
github.com/go-pkgz/lcw v1.1.0/go.mod h1:zwT7RSxFskQsHWHJezYq6n0iTn0n4yIYRDijXq3awM0=
|
||||
github.com/go-pkgz/lgr v0.11.1 h1:hXFhZcznehI6imLhEa379oMOKFz7TQUmisAqb3oLOSM=
|
||||
github.com/go-pkgz/lgr v0.11.1/go.mod h1:tgDF4RXQnBfIgJqjgkv0yOeTQ3F1yewWIZkpUhHnAkU=
|
||||
github.com/go-pkgz/notify v1.1.0 h1:xdMKoY7W5t9lewGzn61yM6Z6oWafPiPS1WtAMO81p2k=
|
||||
@@ -82,14 +81,11 @@ github.com/go-pkgz/notify v1.1.0/go.mod h1:TbqcFbamfJZlhlqXkGz87rCb7sJ0uGm+b7EcM
|
||||
github.com/go-pkgz/repeater v1.1.3 h1:q6+JQF14ESSy28Dd7F+wRelY4F+41HJ0LEy/szNnMiE=
|
||||
github.com/go-pkgz/repeater v1.1.3/go.mod h1:hVTavuO5x3Gxnu8zW7d6sQBfAneKV8X2FjU48kGfpKw=
|
||||
github.com/go-pkgz/rest v1.15.6/go.mod h1:KUWAqbDteYGS/CiXftomQsKjtEOifXsJ36Ka0skYbmk=
|
||||
github.com/go-pkgz/rest v1.18.2 h1:eJYj1qlLJvTx86R4o+XmlKHOAGAX42WeG9PZrJud/e0=
|
||||
github.com/go-pkgz/rest v1.18.2/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
|
||||
github.com/go-pkgz/rest v1.19.0 h1:FNMi5QX5dDIkuC+/e0r+CWsTuOTwUiWMRSA16Ou+9+A=
|
||||
github.com/go-pkgz/rest v1.19.0/go.mod h1:Po+W6zQzpMPP6XDGLdAN2aW7UKk1IyrLSb48Lp1N3oQ=
|
||||
github.com/go-pkgz/syncs v1.3.2 h1:gmioASlJNy3gNosPlgvWOM2QP0Hdjzn2u+/sUShgd8E=
|
||||
github.com/go-pkgz/syncs v1.3.2/go.mod h1:qjgzpp7OpuhDf7BWsW/FHCu9DLjE32NPy6/vXAXT/Cw=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
|
||||
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
@@ -120,11 +116,9 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
@@ -138,13 +132,11 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
|
||||
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
|
||||
@@ -173,28 +165,21 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8
|
||||
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
|
||||
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc=
|
||||
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/redis/go-redis/v9 v9.4.0 h1:Yzoz33UZw9I/mFhx4MNrB6Fk+XHO1VukNcCa1+lwyKk=
|
||||
github.com/redis/go-redis/v9 v9.4.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d h1:l3+2LWCbVxn5itfvXAfH9n4YL9jh8l1g5zcncbIc1cs=
|
||||
github.com/rrivera/identicon v0.0.0-20240116195454-d5ba35832c0d/go.mod h1:TbpErkob6SY7cyozRVSGoB3OlO2qOAgVN8O3KAJ4fMI=
|
||||
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
@@ -204,8 +189,10 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/slack-go/slack v0.12.3 h1:92/dfFU8Q5XP6Wp5rr5/T5JHLM5c5Smtn53fhToAP88=
|
||||
github.com/slack-go/slack v0.12.3/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ=
|
||||
github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/slack-go/slack v0.12.4 h1:4iLT2opw+/QptmQxBNA7S8pNfSIvtn0NDGu7Jq0emi4=
|
||||
github.com/slack-go/slack v0.12.4/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@@ -213,7 +200,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -266,10 +252,9 @@ github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FB
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 h1:k/gmLsJDWwWqbLCur2yWnJzwQEKRcAHXo6seXGuSwWw=
|
||||
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9/go.mod h1:E1AXubJBdNmFERAOucpDIxNzeGfLzg0mYh+UfMWdChA=
|
||||
github.com/yuin/gopher-lua v1.1.0 h1:BojcDhfyDWgU2f2TOzYK/g5p2gxMrku8oupLDqlnSqE=
|
||||
github.com/yuin/gopher-lua v1.1.0/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
||||
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
|
||||
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
|
||||
go.mongodb.org/mongo-driver v1.13.1 h1:YIc7HTYsKndGK4RFzJ3covLz1byri52x0IoMB0Pt/vk=
|
||||
@@ -277,17 +262,14 @@ go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwD
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
|
||||
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -298,9 +280,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
@@ -308,34 +288,29 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
|
||||
golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
|
||||
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
|
||||
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -345,8 +320,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -364,13 +339,10 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
@@ -390,13 +362,10 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
||||
+5
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## v5.0.12 (2024-02-16)
|
||||
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.11...v5.0.12
|
||||
|
||||
|
||||
## v5.0.11 (2023-12-19)
|
||||
|
||||
- History of changes: see https://github.com/go-chi/chi/compare/v5.0.10...v5.0.11
|
||||
|
||||
+2
@@ -354,6 +354,7 @@ with `net/http` can be used with chi's mux.
|
||||
| [RouteHeaders] | Route handling for request headers |
|
||||
| [SetHeader] | Short-hand middleware to set a response header key/value |
|
||||
| [StripSlashes] | Strip slashes on routing paths |
|
||||
| [Sunset] | Sunset set Deprecation/Sunset header to response |
|
||||
| [Throttle] | Puts a ceiling on the number of concurrent requests |
|
||||
| [Timeout] | Signals to the request context when the timeout deadline is reached |
|
||||
| [URLFormat] | Parse extension from url and put it on request context |
|
||||
@@ -380,6 +381,7 @@ with `net/http` can be used with chi's mux.
|
||||
[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders
|
||||
[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader
|
||||
[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes
|
||||
[Sunset]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Sunset
|
||||
[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle
|
||||
[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog
|
||||
[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import "net/http"
|
||||
|
||||
// Maybe middleware will allow you to change the flow of the middleware stack execution depending on return
|
||||
// value of maybeFn(request). This is useful for example if you'd like to skip a middleware handler if
|
||||
// a request does not satisfied the maybeFn logic.
|
||||
// a request does not satisfy the maybeFn logic.
|
||||
func Maybe(mw func(http.Handler) http.Handler, maybeFn func(r *http.Request) bool) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
+16
@@ -107,12 +107,24 @@ func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) {
|
||||
// Handle adds the route `pattern` that matches any http method to
|
||||
// execute the `handler` http.Handler.
|
||||
func (mx *Mux) Handle(pattern string, handler http.Handler) {
|
||||
parts := strings.SplitN(pattern, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
mx.Method(parts[0], parts[1], handler)
|
||||
return
|
||||
}
|
||||
|
||||
mx.handle(mALL, pattern, handler)
|
||||
}
|
||||
|
||||
// HandleFunc adds the route `pattern` that matches any http method to
|
||||
// execute the `handlerFn` http.HandlerFunc.
|
||||
func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) {
|
||||
parts := strings.SplitN(pattern, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
mx.Method(parts[0], parts[1], handlerFn)
|
||||
return
|
||||
}
|
||||
|
||||
mx.handle(mALL, pattern, handlerFn)
|
||||
}
|
||||
|
||||
@@ -440,6 +452,10 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Find the route
|
||||
if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil {
|
||||
if supportsPathValue {
|
||||
setPathValue(rctx, r)
|
||||
}
|
||||
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build go1.22
|
||||
// +build go1.22
|
||||
|
||||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// supportsPathValue is true if the Go version is 1.22 and above.
|
||||
//
|
||||
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
|
||||
const supportsPathValue = true
|
||||
|
||||
// setPathValue sets the path values in the Request value
|
||||
// based on the provided request context.
|
||||
func setPathValue(rctx *Context, r *http.Request) {
|
||||
for i, key := range rctx.URLParams.Keys {
|
||||
value := rctx.URLParams.Values[i]
|
||||
r.SetPathValue(key, value)
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
//go:build !go1.22
|
||||
// +build !go1.22
|
||||
|
||||
package chi
|
||||
|
||||
import "net/http"
|
||||
|
||||
// supportsPathValue is true if the Go version is 1.22 and above.
|
||||
//
|
||||
// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`.
|
||||
const supportsPathValue = false
|
||||
|
||||
// setPathValue sets the path values in the Request value
|
||||
// based on the provided request context.
|
||||
//
|
||||
// setPathValue is only supported in Go 1.22 and above so
|
||||
// this is just a blank function so that it compiles.
|
||||
func setPathValue(rctx *Context, r *http.Request) {
|
||||
}
|
||||
+1
-1
@@ -16,7 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/rest"
|
||||
"github.com/nullrocks/identicon"
|
||||
"github.com/rrivera/identicon"
|
||||
"golang.org/x/image/draw"
|
||||
|
||||
"github.com/go-pkgz/auth/logger"
|
||||
|
||||
-5
@@ -26,18 +26,13 @@ linters:
|
||||
- revive
|
||||
- govet
|
||||
- unconvert
|
||||
- megacheck
|
||||
- structcheck
|
||||
- gas
|
||||
- gocyclo
|
||||
- dupl
|
||||
- misspell
|
||||
- unparam
|
||||
- varcheck
|
||||
- deadcode
|
||||
- typecheck
|
||||
- ineffassign
|
||||
- varcheck
|
||||
- stylecheck
|
||||
- gochecknoinits
|
||||
- exportloopref
|
||||
|
||||
+4
-4
@@ -56,19 +56,19 @@ func NewNopCache() *Nop {
|
||||
}
|
||||
|
||||
// Get calls fn without any caching
|
||||
func (n *Nop) Get(key string, fn func() (interface{}, error)) (interface{}, error) { return fn() }
|
||||
func (n *Nop) Get(_ string, fn func() (interface{}, error)) (interface{}, error) { return fn() }
|
||||
|
||||
// Peek does nothing and always returns false
|
||||
func (n *Nop) Peek(key string) (interface{}, bool) { return nil, false }
|
||||
func (n *Nop) Peek(string) (interface{}, bool) { return nil, false }
|
||||
|
||||
// Invalidate does nothing for nop cache
|
||||
func (n *Nop) Invalidate(fn func(key string) bool) {}
|
||||
func (n *Nop) Invalidate(func(key string) bool) {}
|
||||
|
||||
// Purge does nothing for nop cache
|
||||
func (n *Nop) Purge() {}
|
||||
|
||||
// Delete does nothing for nop cache
|
||||
func (n *Nop) Delete(key string) {}
|
||||
func (n *Nop) Delete(string) {}
|
||||
|
||||
// Keys does nothing for nop cache
|
||||
func (n *Nop) Keys() []string { return nil }
|
||||
|
||||
+2
-2
@@ -14,11 +14,11 @@ type PubSub interface {
|
||||
type NopPubSub struct{}
|
||||
|
||||
// Subscribe does nothing for NopPubSub
|
||||
func (n *NopPubSub) Subscribe(fn func(fromID, key string)) error {
|
||||
func (n *NopPubSub) Subscribe(func(fromID string, key string)) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Publish does nothing for NopPubSub
|
||||
func (n *NopPubSub) Publish(fromID, key string) error {
|
||||
func (n *NopPubSub) Publish(string, string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// RedisValueSizeLimit is maximum allowed value size in Redis
|
||||
|
||||
+1
-1
@@ -6,8 +6,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// New parses uri and makes any of supported caches
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ func BlackWords(words ...string) func(http.Handler) http.Handler {
|
||||
body := strings.ToLower(string(content))
|
||||
r.Body = io.NopCloser(bytes.NewReader(content))
|
||||
|
||||
if len(body) > 0 {
|
||||
if body != "" {
|
||||
for _, word := range words {
|
||||
if strings.Contains(body, strings.ToLower(word)) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
|
||||
+1
-1
@@ -226,7 +226,7 @@ func (l *Middleware) getBody(r *http.Request) string {
|
||||
// https://golang.org/pkg/net/http/#Handler
|
||||
r.Body = io.NopCloser(reader)
|
||||
|
||||
if len(body) > 0 {
|
||||
if body != "" {
|
||||
body = strings.Replace(body, "\n", " ", -1)
|
||||
body = reMultWhtsp.ReplaceAllString(body, " ")
|
||||
}
|
||||
|
||||
+19
-1
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
// JSON is a map alias, just for convenience
|
||||
type JSON map[string]interface{}
|
||||
type JSON map[string]any
|
||||
|
||||
// RenderJSON sends data as json
|
||||
func RenderJSON(w http.ResponseWriter, data interface{}) {
|
||||
@@ -97,3 +97,21 @@ func ParseFromTo(r *http.Request) (from, to time.Time, err error) {
|
||||
}
|
||||
return from, to, nil
|
||||
}
|
||||
|
||||
// DecodeJSON decodes json request from http.Request to given type
|
||||
func DecodeJSON[T any](r *http.Request, res *T) error {
|
||||
if err := json.NewDecoder(r.Body).Decode(&res); err != nil {
|
||||
return fmt.Errorf("decode json: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EncodeJSON encodes given type to http.ResponseWriter and sets status code and content type header
|
||||
func EncodeJSON[T any](w http.ResponseWriter, status int, v T) error {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.WriteHeader(status)
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
return fmt.Errorf("encode json: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
*.rdb
|
||||
testdata/*/
|
||||
.idea/
|
||||
-177
@@ -1,177 +0,0 @@
|
||||
## [8.11.5](https://github.com/go-redis/redis/compare/v8.11.4...v8.11.5) (2022-03-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add missing Expire methods to Cmdable ([17e3b43](https://github.com/go-redis/redis/commit/17e3b43879d516437ada71cf9c0deac6a382ed9a))
|
||||
* add whitespace for avoid unlikely colisions ([7f7c181](https://github.com/go-redis/redis/commit/7f7c1817617cfec909efb13d14ad22ef05a6ad4c))
|
||||
* example/otel compile error ([#2028](https://github.com/go-redis/redis/issues/2028)) ([187c07c](https://github.com/go-redis/redis/commit/187c07c41bf68dc3ab280bc3a925e960bbef6475))
|
||||
* **extra/redisotel:** set span.kind attribute to client ([065b200](https://github.com/go-redis/redis/commit/065b200070b41e6e949710b4f9e01b50ccc60ab2))
|
||||
* format ([96f53a0](https://github.com/go-redis/redis/commit/96f53a0159a28affa94beec1543a62234e7f8b32))
|
||||
* invalid type assert in stringArg ([de6c131](https://github.com/go-redis/redis/commit/de6c131865b8263400c8491777b295035f2408e4))
|
||||
* rename Golang to Go ([#2030](https://github.com/go-redis/redis/issues/2030)) ([b82a2d9](https://github.com/go-redis/redis/commit/b82a2d9d4d2de7b7cbe8fcd4895be62dbcacacbc))
|
||||
* set timeout for WAIT command. Fixes [#1963](https://github.com/go-redis/redis/issues/1963) ([333fee1](https://github.com/go-redis/redis/commit/333fee1a8fd98a2fbff1ab187c1b03246a7eb01f))
|
||||
* update some argument counts in pre-allocs ([f6974eb](https://github.com/go-redis/redis/commit/f6974ebb5c40a8adf90d2cacab6dc297f4eba4c2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add redis v7's NX, XX, GT, LT expire variants ([e19bbb2](https://github.com/go-redis/redis/commit/e19bbb26e2e395c6e077b48d80d79e99f729a8b8))
|
||||
* add support for acl sentinel auth in universal client ([ab0ccc4](https://github.com/go-redis/redis/commit/ab0ccc47413f9b2a6eabc852fed5005a3ee1af6e))
|
||||
* add support for COPY command ([#2016](https://github.com/go-redis/redis/issues/2016)) ([730afbc](https://github.com/go-redis/redis/commit/730afbcffb93760e8a36cc06cfe55ab102b693a7))
|
||||
* add support for passing extra attributes added to spans ([39faaa1](https://github.com/go-redis/redis/commit/39faaa171523834ba527c9789710c4fde87f5a2e))
|
||||
* add support for time.Duration write and scan ([2f1b74e](https://github.com/go-redis/redis/commit/2f1b74e20cdd7719b2aecf0768d3e3ae7c3e781b))
|
||||
* **redisotel:** ability to override TracerProvider ([#1998](https://github.com/go-redis/redis/issues/1998)) ([bf8d4aa](https://github.com/go-redis/redis/commit/bf8d4aa60c00366cda2e98c3ddddc8cf68507417))
|
||||
* set net.peer.name and net.peer.port in otel example ([69bf454](https://github.com/go-redis/redis/commit/69bf454f706204211cd34835f76b2e8192d3766d))
|
||||
|
||||
|
||||
|
||||
## [8.11.4](https://github.com/go-redis/redis/compare/v8.11.3...v8.11.4) (2021-10-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add acl auth support for sentinels ([f66582f](https://github.com/go-redis/redis/commit/f66582f44f3dc3a4705a5260f982043fde4aa634))
|
||||
* add Cmd.{String,Int,Float,Bool}Slice helpers and an example ([5d3d293](https://github.com/go-redis/redis/commit/5d3d293cc9c60b90871e2420602001463708ce24))
|
||||
* add SetVal method for each command ([168981d](https://github.com/go-redis/redis/commit/168981da2d84ee9e07d15d3e74d738c162e264c4))
|
||||
|
||||
|
||||
|
||||
## v8.11
|
||||
|
||||
- Remove OpenTelemetry metrics.
|
||||
- Supports more redis commands and options.
|
||||
|
||||
## v8.10
|
||||
|
||||
- Removed extra OpenTelemetry spans from go-redis core. Now go-redis instrumentation only adds a
|
||||
single span with a Redis command (instead of 4 spans). There are multiple reasons behind this
|
||||
decision:
|
||||
|
||||
- Traces become smaller and less noisy.
|
||||
- It may be costly to process those 3 extra spans for each query.
|
||||
- go-redis no longer depends on OpenTelemetry.
|
||||
|
||||
Eventually we hope to replace the information that we no longer collect with OpenTelemetry
|
||||
Metrics.
|
||||
|
||||
## v8.9
|
||||
|
||||
- Changed `PubSub.Channel` to only rely on `Ping` result. You can now use `WithChannelSize`,
|
||||
`WithChannelHealthCheckInterval`, and `WithChannelSendTimeout` to override default settings.
|
||||
|
||||
## v8.8
|
||||
|
||||
- To make updating easier, extra modules now have the same version as go-redis does. That means that
|
||||
you need to update your imports:
|
||||
|
||||
```
|
||||
github.com/go-redis/redis/extra/redisotel -> github.com/go-redis/redis/extra/redisotel/v8
|
||||
github.com/go-redis/redis/extra/rediscensus -> github.com/go-redis/redis/extra/rediscensus/v8
|
||||
```
|
||||
|
||||
## v8.5
|
||||
|
||||
- [knadh](https://github.com/knadh) contributed long-awaited ability to scan Redis Hash into a
|
||||
struct:
|
||||
|
||||
```go
|
||||
err := rdb.HGetAll(ctx, "hash").Scan(&data)
|
||||
|
||||
err := rdb.MGet(ctx, "key1", "key2").Scan(&data)
|
||||
```
|
||||
|
||||
- Please check [redismock](https://github.com/go-redis/redismock) by
|
||||
[monkey92t](https://github.com/monkey92t) if you are looking for mocking Redis Client.
|
||||
|
||||
## v8
|
||||
|
||||
- All commands require `context.Context` as a first argument, e.g. `rdb.Ping(ctx)`. If you are not
|
||||
using `context.Context` yet, the simplest option is to define global package variable
|
||||
`var ctx = context.TODO()` and use it when `ctx` is required.
|
||||
|
||||
- Full support for `context.Context` canceling.
|
||||
|
||||
- Added `redis.NewFailoverClusterClient` that supports routing read-only commands to a slave node.
|
||||
|
||||
- Added `redisext.OpenTemetryHook` that adds
|
||||
[Redis OpenTelemetry instrumentation](https://redis.uptrace.dev/tracing/).
|
||||
|
||||
- Redis slow log support.
|
||||
|
||||
- Ring uses Rendezvous Hashing by default which provides better distribution. You need to move
|
||||
existing keys to a new location or keys will be inaccessible / lost. To use old hashing scheme:
|
||||
|
||||
```go
|
||||
import "github.com/golang/groupcache/consistenthash"
|
||||
|
||||
ring := redis.NewRing(&redis.RingOptions{
|
||||
NewConsistentHash: func() {
|
||||
return consistenthash.New(100, crc32.ChecksumIEEE)
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
- `ClusterOptions.MaxRedirects` default value is changed from 8 to 3.
|
||||
- `Options.MaxRetries` default value is changed from 0 to 3.
|
||||
|
||||
- `Cluster.ForEachNode` is renamed to `ForEachShard` for consistency with `Ring`.
|
||||
|
||||
## v7.3
|
||||
|
||||
- New option `Options.Username` which causes client to use `AuthACL`. Be aware if your connection
|
||||
URL contains username.
|
||||
|
||||
## v7.2
|
||||
|
||||
- Existing `HMSet` is renamed to `HSet` and old deprecated `HMSet` is restored for Redis 3 users.
|
||||
|
||||
## v7.1
|
||||
|
||||
- Existing `Cmd.String` is renamed to `Cmd.Text`. New `Cmd.String` implements `fmt.Stringer`
|
||||
interface.
|
||||
|
||||
## v7
|
||||
|
||||
- _Important_. Tx.Pipeline now returns a non-transactional pipeline. Use Tx.TxPipeline for a
|
||||
transactional pipeline.
|
||||
- WrapProcess is replaced with more convenient AddHook that has access to context.Context.
|
||||
- WithContext now can not be used to create a shallow copy of the client.
|
||||
- New methods ProcessContext, DoContext, and ExecContext.
|
||||
- Client respects Context.Deadline when setting net.Conn deadline.
|
||||
- Client listens on Context.Done while waiting for a connection from the pool and returns an error
|
||||
when context context is cancelled.
|
||||
- Add PubSub.ChannelWithSubscriptions that sends `*Subscription` in addition to `*Message` to allow
|
||||
detecting reconnections.
|
||||
- `time.Time` is now marshalled in RFC3339 format. `rdb.Get("foo").Time()` helper is added to parse
|
||||
the time.
|
||||
- `SetLimiter` is removed and added `Options.Limiter` instead.
|
||||
- `HMSet` is deprecated as of Redis v4.
|
||||
|
||||
## v6.15
|
||||
|
||||
- Cluster and Ring pipelines process commands for each node in its own goroutine.
|
||||
|
||||
## 6.14
|
||||
|
||||
- Added Options.MinIdleConns.
|
||||
- Added Options.MaxConnAge.
|
||||
- PoolStats.FreeConns is renamed to PoolStats.IdleConns.
|
||||
- Add Client.Do to simplify creating custom commands.
|
||||
- Add Cmd.String, Cmd.Int, Cmd.Int64, Cmd.Uint64, Cmd.Float64, and Cmd.Bool helpers.
|
||||
- Lower memory usage.
|
||||
|
||||
## v6.13
|
||||
|
||||
- Ring got new options called `HashReplicas` and `Hash`. It is recommended to set
|
||||
`HashReplicas = 1000` for better keys distribution between shards.
|
||||
- Cluster client was optimized to use much less memory when reloading cluster state.
|
||||
- PubSub.ReceiveMessage is re-worked to not use ReceiveTimeout so it does not lose data when timeout
|
||||
occurres. In most cases it is recommended to use PubSub.Channel instead.
|
||||
- Dialer.KeepAlive is set to 5 minutes by default.
|
||||
|
||||
## v6.12
|
||||
|
||||
- ClusterClient got new option called `ClusterSlots` which allows to build cluster of normal Redis
|
||||
Servers that don't have cluster mode enabled. See
|
||||
https://godoc.org/github.com/go-redis/redis#example-NewClusterClient--ManualSetup
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
PACKAGE_DIRS := $(shell find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; | sort)
|
||||
|
||||
test: testdeps
|
||||
go test ./...
|
||||
go test ./... -short -race
|
||||
go test ./... -run=NONE -bench=. -benchmem
|
||||
env GOOS=linux GOARCH=386 go test ./...
|
||||
go vet
|
||||
|
||||
testdeps: testdata/redis/src/redis-server
|
||||
|
||||
bench: testdeps
|
||||
go test ./... -test.run=NONE -test.bench=. -test.benchmem
|
||||
|
||||
.PHONY: all test testdeps bench
|
||||
|
||||
testdata/redis:
|
||||
mkdir -p $@
|
||||
wget -qO- https://download.redis.io/releases/redis-6.2.5.tar.gz | tar xvz --strip-components=1 -C $@
|
||||
|
||||
testdata/redis/src/redis-server: testdata/redis
|
||||
cd $< && make all
|
||||
|
||||
fmt:
|
||||
gofmt -w -s ./
|
||||
goimports -w -local github.com/go-redis/redis ./
|
||||
|
||||
go_mod_tidy:
|
||||
go get -u && go mod tidy
|
||||
set -e; for dir in $(PACKAGE_DIRS); do \
|
||||
echo "go mod tidy in $${dir}"; \
|
||||
(cd "$${dir}" && \
|
||||
go get -u && \
|
||||
go mod tidy); \
|
||||
done
|
||||
-175
@@ -1,175 +0,0 @@
|
||||
# Redis client for Go
|
||||
|
||||

|
||||
[](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc)
|
||||
[](https://redis.uptrace.dev/)
|
||||
|
||||
go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
|
||||
Uptrace is an open source and blazingly fast **distributed tracing** backend powered by
|
||||
OpenTelemetry and ClickHouse. Give it a star as well!
|
||||
|
||||
## Resources
|
||||
|
||||
- [Discussions](https://github.com/go-redis/redis/discussions)
|
||||
- [Documentation](https://redis.uptrace.dev)
|
||||
- [Reference](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc)
|
||||
- [Examples](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#pkg-examples)
|
||||
- [RealWorld example app](https://github.com/uptrace/go-treemux-realworld-example-app)
|
||||
|
||||
Other projects you may like:
|
||||
|
||||
- [Bun](https://bun.uptrace.dev) - fast and simple SQL client for PostgreSQL, MySQL, and SQLite.
|
||||
- [BunRouter](https://bunrouter.uptrace.dev/) - fast and flexible HTTP router for Go.
|
||||
|
||||
## Ecosystem
|
||||
|
||||
- [Redis Mock](https://github.com/go-redis/redismock)
|
||||
- [Distributed Locks](https://github.com/bsm/redislock)
|
||||
- [Redis Cache](https://github.com/go-redis/cache)
|
||||
- [Rate limiting](https://github.com/go-redis/redis_rate)
|
||||
|
||||
## Features
|
||||
|
||||
- Redis 3 commands except QUIT, MONITOR, and SYNC.
|
||||
- Automatic connection pooling with
|
||||
[circuit breaker](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern) support.
|
||||
- [Pub/Sub](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#PubSub).
|
||||
- [Transactions](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client-TxPipeline).
|
||||
- [Pipeline](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client.Pipeline) and
|
||||
[TxPipeline](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client.TxPipeline).
|
||||
- [Scripting](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#Script).
|
||||
- [Timeouts](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#Options).
|
||||
- [Redis Sentinel](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewFailoverClient).
|
||||
- [Redis Cluster](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewClusterClient).
|
||||
- [Cluster of Redis Servers](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-NewClusterClient-ManualSetup)
|
||||
without using cluster mode and Redis Sentinel.
|
||||
- [Ring](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewRing).
|
||||
- [Instrumentation](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-package-Instrumentation).
|
||||
|
||||
## Installation
|
||||
|
||||
go-redis supports 2 last Go versions and requires a Go version with
|
||||
[modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go
|
||||
module:
|
||||
|
||||
```shell
|
||||
go mod init github.com/my/repo
|
||||
```
|
||||
|
||||
And then install go-redis/v8 (note _v8_ in the import; omitting it is a popular mistake):
|
||||
|
||||
```shell
|
||||
go get github.com/go-redis/redis/v8
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"github.com/go-redis/redis/v8"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
func ExampleClient() {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
})
|
||||
|
||||
err := rdb.Set(ctx, "key", "value", 0).Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
val, err := rdb.Get(ctx, "key").Result()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("key", val)
|
||||
|
||||
val2, err := rdb.Get(ctx, "key2").Result()
|
||||
if err == redis.Nil {
|
||||
fmt.Println("key2 does not exist")
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
fmt.Println("key2", val2)
|
||||
}
|
||||
// Output: key value
|
||||
// key2 does not exist
|
||||
}
|
||||
```
|
||||
|
||||
## Look and feel
|
||||
|
||||
Some corner cases:
|
||||
|
||||
```go
|
||||
// SET key value EX 10 NX
|
||||
set, err := rdb.SetNX(ctx, "key", "value", 10*time.Second).Result()
|
||||
|
||||
// SET key value keepttl NX
|
||||
set, err := rdb.SetNX(ctx, "key", "value", redis.KeepTTL).Result()
|
||||
|
||||
// SORT list LIMIT 0 2 ASC
|
||||
vals, err := rdb.Sort(ctx, "list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result()
|
||||
|
||||
// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2
|
||||
vals, err := rdb.ZRangeByScoreWithScores(ctx, "zset", &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: "+inf",
|
||||
Offset: 0,
|
||||
Count: 2,
|
||||
}).Result()
|
||||
|
||||
// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM
|
||||
vals, err := rdb.ZInterStore(ctx, "out", &redis.ZStore{
|
||||
Keys: []string{"zset1", "zset2"},
|
||||
Weights: []int64{2, 3}
|
||||
}).Result()
|
||||
|
||||
// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello"
|
||||
vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result()
|
||||
|
||||
// custom command
|
||||
res, err := rdb.Do(ctx, "set", "key", "value").Result()
|
||||
```
|
||||
|
||||
## Run the test
|
||||
|
||||
go-redis will start a redis-server and run the test cases.
|
||||
|
||||
The paths of redis-server bin file and redis config file are defined in `main_test.go`:
|
||||
|
||||
```
|
||||
var (
|
||||
redisServerBin, _ = filepath.Abs(filepath.Join("testdata", "redis", "src", "redis-server"))
|
||||
redisServerConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "redis.conf"))
|
||||
)
|
||||
```
|
||||
|
||||
For local testing, you can change the variables to refer to your local files, or create a soft link
|
||||
to the corresponding folder for redis-server and copy the config file to `testdata/redis/`:
|
||||
|
||||
```
|
||||
ln -s /usr/bin/redis-server ./go-redis/testdata/redis/src
|
||||
cp ./go-redis/testdata/redis.conf ./go-redis/testdata/redis/
|
||||
```
|
||||
|
||||
Lastly, run:
|
||||
|
||||
```
|
||||
go test
|
||||
```
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all the people who already contributed!
|
||||
|
||||
<a href="https://github.com/go-redis/redis/graphs/contributors">
|
||||
<img src="https://contributors-img.web.app/image?repo=go-redis/redis" />
|
||||
</a>
|
||||
-3478
File diff suppressed because it is too large
Load Diff
-3475
File diff suppressed because it is too large
Load Diff
-332
@@ -1,332 +0,0 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/util"
|
||||
)
|
||||
|
||||
// redis resp protocol data type.
|
||||
const (
|
||||
ErrorReply = '-'
|
||||
StatusReply = '+'
|
||||
IntReply = ':'
|
||||
StringReply = '$'
|
||||
ArrayReply = '*'
|
||||
)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const Nil = RedisError("redis: nil") // nolint:errname
|
||||
|
||||
type RedisError string
|
||||
|
||||
func (e RedisError) Error() string { return string(e) }
|
||||
|
||||
func (RedisError) RedisError() {}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type MultiBulkParse func(*Reader, int64) (interface{}, error)
|
||||
|
||||
type Reader struct {
|
||||
rd *bufio.Reader
|
||||
_buf []byte
|
||||
}
|
||||
|
||||
func NewReader(rd io.Reader) *Reader {
|
||||
return &Reader{
|
||||
rd: bufio.NewReader(rd),
|
||||
_buf: make([]byte, 64),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) Buffered() int {
|
||||
return r.rd.Buffered()
|
||||
}
|
||||
|
||||
func (r *Reader) Peek(n int) ([]byte, error) {
|
||||
return r.rd.Peek(n)
|
||||
}
|
||||
|
||||
func (r *Reader) Reset(rd io.Reader) {
|
||||
r.rd.Reset(rd)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadLine() ([]byte, error) {
|
||||
line, err := r.readLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isNilReply(line) {
|
||||
return nil, Nil
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
|
||||
// readLine that returns an error if:
|
||||
// - there is a pending read error;
|
||||
// - or line does not end with \r\n.
|
||||
func (r *Reader) readLine() ([]byte, error) {
|
||||
b, err := r.rd.ReadSlice('\n')
|
||||
if err != nil {
|
||||
if err != bufio.ErrBufferFull {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
full := make([]byte, len(b))
|
||||
copy(full, b)
|
||||
|
||||
b, err = r.rd.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
full = append(full, b...) //nolint:makezero
|
||||
b = full
|
||||
}
|
||||
if len(b) <= 2 || b[len(b)-1] != '\n' || b[len(b)-2] != '\r' {
|
||||
return nil, fmt.Errorf("redis: invalid reply: %q", b)
|
||||
}
|
||||
return b[:len(b)-2], nil
|
||||
}
|
||||
|
||||
func (r *Reader) ReadReply(m MultiBulkParse) (interface{}, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch line[0] {
|
||||
case ErrorReply:
|
||||
return nil, ParseErrorReply(line)
|
||||
case StatusReply:
|
||||
return string(line[1:]), nil
|
||||
case IntReply:
|
||||
return util.ParseInt(line[1:], 10, 64)
|
||||
case StringReply:
|
||||
return r.readStringReply(line)
|
||||
case ArrayReply:
|
||||
n, err := parseArrayLen(line)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m == nil {
|
||||
err := fmt.Errorf("redis: got %.100q, but multi bulk parser is nil", line)
|
||||
return nil, err
|
||||
}
|
||||
return m(r, n)
|
||||
}
|
||||
return nil, fmt.Errorf("redis: can't parse %.100q", line)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadIntReply() (int64, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch line[0] {
|
||||
case ErrorReply:
|
||||
return 0, ParseErrorReply(line)
|
||||
case IntReply:
|
||||
return util.ParseInt(line[1:], 10, 64)
|
||||
default:
|
||||
return 0, fmt.Errorf("redis: can't parse int reply: %.100q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) ReadString() (string, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch line[0] {
|
||||
case ErrorReply:
|
||||
return "", ParseErrorReply(line)
|
||||
case StringReply:
|
||||
return r.readStringReply(line)
|
||||
case StatusReply:
|
||||
return string(line[1:]), nil
|
||||
case IntReply:
|
||||
return string(line[1:]), nil
|
||||
default:
|
||||
return "", fmt.Errorf("redis: can't parse reply=%.100q reading string", line)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) readStringReply(line []byte) (string, error) {
|
||||
if isNilReply(line) {
|
||||
return "", Nil
|
||||
}
|
||||
|
||||
replyLen, err := util.Atoi(line[1:])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b := make([]byte, replyLen+2)
|
||||
_, err = io.ReadFull(r.rd, b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return util.BytesToString(b[:replyLen]), nil
|
||||
}
|
||||
|
||||
func (r *Reader) ReadArrayReply(m MultiBulkParse) (interface{}, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch line[0] {
|
||||
case ErrorReply:
|
||||
return nil, ParseErrorReply(line)
|
||||
case ArrayReply:
|
||||
n, err := parseArrayLen(line)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m(r, n)
|
||||
default:
|
||||
return nil, fmt.Errorf("redis: can't parse array reply: %.100q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) ReadArrayLen() (int, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch line[0] {
|
||||
case ErrorReply:
|
||||
return 0, ParseErrorReply(line)
|
||||
case ArrayReply:
|
||||
n, err := parseArrayLen(line)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return int(n), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("redis: can't parse array reply: %.100q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) ReadScanReply() ([]string, uint64, error) {
|
||||
n, err := r.ReadArrayLen()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if n != 2 {
|
||||
return nil, 0, fmt.Errorf("redis: got %d elements in scan reply, expected 2", n)
|
||||
}
|
||||
|
||||
cursor, err := r.ReadUint()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
n, err = r.ReadArrayLen()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
keys := make([]string, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
key, err := r.ReadString()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
keys[i] = key
|
||||
}
|
||||
|
||||
return keys, cursor, err
|
||||
}
|
||||
|
||||
func (r *Reader) ReadInt() (int64, error) {
|
||||
b, err := r.readTmpBytesReply()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return util.ParseInt(b, 10, 64)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint() (uint64, error) {
|
||||
b, err := r.readTmpBytesReply()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return util.ParseUint(b, 10, 64)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadFloatReply() (float64, error) {
|
||||
b, err := r.readTmpBytesReply()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return util.ParseFloat(b, 64)
|
||||
}
|
||||
|
||||
func (r *Reader) readTmpBytesReply() ([]byte, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch line[0] {
|
||||
case ErrorReply:
|
||||
return nil, ParseErrorReply(line)
|
||||
case StringReply:
|
||||
return r._readTmpBytesReply(line)
|
||||
case StatusReply:
|
||||
return line[1:], nil
|
||||
default:
|
||||
return nil, fmt.Errorf("redis: can't parse string reply: %.100q", line)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) _readTmpBytesReply(line []byte) ([]byte, error) {
|
||||
if isNilReply(line) {
|
||||
return nil, Nil
|
||||
}
|
||||
|
||||
replyLen, err := util.Atoi(line[1:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := r.buf(replyLen + 2)
|
||||
_, err = io.ReadFull(r.rd, buf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf[:replyLen], nil
|
||||
}
|
||||
|
||||
func (r *Reader) buf(n int) []byte {
|
||||
if n <= cap(r._buf) {
|
||||
return r._buf[:n]
|
||||
}
|
||||
d := n - cap(r._buf)
|
||||
r._buf = append(r._buf, make([]byte, d)...)
|
||||
return r._buf
|
||||
}
|
||||
|
||||
func isNilReply(b []byte) bool {
|
||||
return len(b) == 3 &&
|
||||
(b[0] == StringReply || b[0] == ArrayReply) &&
|
||||
b[1] == '-' && b[2] == '1'
|
||||
}
|
||||
|
||||
func ParseErrorReply(line []byte) error {
|
||||
return RedisError(string(line[1:]))
|
||||
}
|
||||
|
||||
func parseArrayLen(line []byte) (int64, error) {
|
||||
if isNilReply(line) {
|
||||
return 0, Nil
|
||||
}
|
||||
return util.ParseInt(line[1:], 10, 64)
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
//go:build appengine
|
||||
// +build appengine
|
||||
|
||||
package internal
|
||||
|
||||
func String(b []byte) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func Bytes(s string) []byte {
|
||||
return []byte(s)
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
//go:build !appengine
|
||||
// +build !appengine
|
||||
|
||||
package internal
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// String converts byte slice to string.
|
||||
func String(b []byte) string {
|
||||
return *(*string)(unsafe.Pointer(&b))
|
||||
}
|
||||
|
||||
// Bytes converts string to byte slice.
|
||||
func Bytes(s string) []byte {
|
||||
return *(*[]byte)(unsafe.Pointer(
|
||||
&struct {
|
||||
string
|
||||
Cap int
|
||||
}{s, len(s)},
|
||||
))
|
||||
}
|
||||
-773
@@ -1,773 +0,0 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal"
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/go-redis/redis/v8/internal/proto"
|
||||
)
|
||||
|
||||
// Nil reply returned by Redis when key does not exist.
|
||||
const Nil = proto.Nil
|
||||
|
||||
func SetLogger(logger internal.Logging) {
|
||||
internal.Logger = logger
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type Hook interface {
|
||||
BeforeProcess(ctx context.Context, cmd Cmder) (context.Context, error)
|
||||
AfterProcess(ctx context.Context, cmd Cmder) error
|
||||
|
||||
BeforeProcessPipeline(ctx context.Context, cmds []Cmder) (context.Context, error)
|
||||
AfterProcessPipeline(ctx context.Context, cmds []Cmder) error
|
||||
}
|
||||
|
||||
type hooks struct {
|
||||
hooks []Hook
|
||||
}
|
||||
|
||||
func (hs *hooks) lock() {
|
||||
hs.hooks = hs.hooks[:len(hs.hooks):len(hs.hooks)]
|
||||
}
|
||||
|
||||
func (hs hooks) clone() hooks {
|
||||
clone := hs
|
||||
clone.lock()
|
||||
return clone
|
||||
}
|
||||
|
||||
func (hs *hooks) AddHook(hook Hook) {
|
||||
hs.hooks = append(hs.hooks, hook)
|
||||
}
|
||||
|
||||
func (hs hooks) process(
|
||||
ctx context.Context, cmd Cmder, fn func(context.Context, Cmder) error,
|
||||
) error {
|
||||
if len(hs.hooks) == 0 {
|
||||
err := fn(ctx, cmd)
|
||||
cmd.SetErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
var hookIndex int
|
||||
var retErr error
|
||||
|
||||
for ; hookIndex < len(hs.hooks) && retErr == nil; hookIndex++ {
|
||||
ctx, retErr = hs.hooks[hookIndex].BeforeProcess(ctx, cmd)
|
||||
if retErr != nil {
|
||||
cmd.SetErr(retErr)
|
||||
}
|
||||
}
|
||||
|
||||
if retErr == nil {
|
||||
retErr = fn(ctx, cmd)
|
||||
cmd.SetErr(retErr)
|
||||
}
|
||||
|
||||
for hookIndex--; hookIndex >= 0; hookIndex-- {
|
||||
if err := hs.hooks[hookIndex].AfterProcess(ctx, cmd); err != nil {
|
||||
retErr = err
|
||||
cmd.SetErr(retErr)
|
||||
}
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
func (hs hooks) processPipeline(
|
||||
ctx context.Context, cmds []Cmder, fn func(context.Context, []Cmder) error,
|
||||
) error {
|
||||
if len(hs.hooks) == 0 {
|
||||
err := fn(ctx, cmds)
|
||||
return err
|
||||
}
|
||||
|
||||
var hookIndex int
|
||||
var retErr error
|
||||
|
||||
for ; hookIndex < len(hs.hooks) && retErr == nil; hookIndex++ {
|
||||
ctx, retErr = hs.hooks[hookIndex].BeforeProcessPipeline(ctx, cmds)
|
||||
if retErr != nil {
|
||||
setCmdsErr(cmds, retErr)
|
||||
}
|
||||
}
|
||||
|
||||
if retErr == nil {
|
||||
retErr = fn(ctx, cmds)
|
||||
}
|
||||
|
||||
for hookIndex--; hookIndex >= 0; hookIndex-- {
|
||||
if err := hs.hooks[hookIndex].AfterProcessPipeline(ctx, cmds); err != nil {
|
||||
retErr = err
|
||||
setCmdsErr(cmds, retErr)
|
||||
}
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
func (hs hooks) processTxPipeline(
|
||||
ctx context.Context, cmds []Cmder, fn func(context.Context, []Cmder) error,
|
||||
) error {
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
return hs.processPipeline(ctx, cmds, fn)
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type baseClient struct {
|
||||
opt *Options
|
||||
connPool pool.Pooler
|
||||
|
||||
onClose func() error // hook called when client is closed
|
||||
}
|
||||
|
||||
func newBaseClient(opt *Options, connPool pool.Pooler) *baseClient {
|
||||
return &baseClient{
|
||||
opt: opt,
|
||||
connPool: connPool,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *baseClient) clone() *baseClient {
|
||||
clone := *c
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (c *baseClient) withTimeout(timeout time.Duration) *baseClient {
|
||||
opt := c.opt.clone()
|
||||
opt.ReadTimeout = timeout
|
||||
opt.WriteTimeout = timeout
|
||||
|
||||
clone := c.clone()
|
||||
clone.opt = opt
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
func (c *baseClient) String() string {
|
||||
return fmt.Sprintf("Redis<%s db:%d>", c.getAddr(), c.opt.DB)
|
||||
}
|
||||
|
||||
func (c *baseClient) newConn(ctx context.Context) (*pool.Conn, error) {
|
||||
cn, err := c.connPool.NewConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.initConn(ctx, cn)
|
||||
if err != nil {
|
||||
_ = c.connPool.CloseConn(cn)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) getConn(ctx context.Context) (*pool.Conn, error) {
|
||||
if c.opt.Limiter != nil {
|
||||
err := c.opt.Limiter.Allow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cn, err := c._getConn(ctx)
|
||||
if err != nil {
|
||||
if c.opt.Limiter != nil {
|
||||
c.opt.Limiter.ReportResult(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) {
|
||||
cn, err := c.connPool.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cn.Inited {
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
if err := c.initConn(ctx, cn); err != nil {
|
||||
c.connPool.Remove(ctx, cn, err)
|
||||
if err := errors.Unwrap(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
|
||||
if cn.Inited {
|
||||
return nil
|
||||
}
|
||||
cn.Inited = true
|
||||
|
||||
if c.opt.Password == "" &&
|
||||
c.opt.DB == 0 &&
|
||||
!c.opt.readOnly &&
|
||||
c.opt.OnConnect == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
connPool := pool.NewSingleConnPool(c.connPool, cn)
|
||||
conn := newConn(ctx, c.opt, connPool)
|
||||
|
||||
_, err := conn.Pipelined(ctx, func(pipe Pipeliner) error {
|
||||
if c.opt.Password != "" {
|
||||
if c.opt.Username != "" {
|
||||
pipe.AuthACL(ctx, c.opt.Username, c.opt.Password)
|
||||
} else {
|
||||
pipe.Auth(ctx, c.opt.Password)
|
||||
}
|
||||
}
|
||||
|
||||
if c.opt.DB > 0 {
|
||||
pipe.Select(ctx, c.opt.DB)
|
||||
}
|
||||
|
||||
if c.opt.readOnly {
|
||||
pipe.ReadOnly(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.opt.OnConnect != nil {
|
||||
return c.opt.OnConnect(ctx, conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) {
|
||||
if c.opt.Limiter != nil {
|
||||
c.opt.Limiter.ReportResult(err)
|
||||
}
|
||||
|
||||
if isBadConn(err, false, c.opt.Addr) {
|
||||
c.connPool.Remove(ctx, cn, err)
|
||||
} else {
|
||||
c.connPool.Put(ctx, cn)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *baseClient) withConn(
|
||||
ctx context.Context, fn func(context.Context, *pool.Conn) error,
|
||||
) error {
|
||||
cn, err := c.getConn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
c.releaseConn(ctx, cn, err)
|
||||
}()
|
||||
|
||||
done := ctx.Done() //nolint:ifshort
|
||||
|
||||
if done == nil {
|
||||
err = fn(ctx, cn)
|
||||
return err
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() { errc <- fn(ctx, cn) }()
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
_ = cn.Close()
|
||||
// Wait for the goroutine to finish and send something.
|
||||
<-errc
|
||||
|
||||
err = ctx.Err()
|
||||
return err
|
||||
case err = <-errc:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
|
||||
attempt := attempt
|
||||
|
||||
retry, err := c._process(ctx, cmd, attempt)
|
||||
if err == nil || !retry {
|
||||
return err
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) {
|
||||
if attempt > 0 {
|
||||
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
retryTimeout := uint32(1)
|
||||
err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmd(wr, cmd)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cn.WithReader(ctx, c.cmdTimeout(cmd), cmd.readReply)
|
||||
if err != nil {
|
||||
if cmd.readTimeout() == nil {
|
||||
atomic.StoreUint32(&retryTimeout, 1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
retry := shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1)
|
||||
return retry, err
|
||||
}
|
||||
|
||||
func (c *baseClient) retryBackoff(attempt int) time.Duration {
|
||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
|
||||
}
|
||||
|
||||
func (c *baseClient) cmdTimeout(cmd Cmder) time.Duration {
|
||||
if timeout := cmd.readTimeout(); timeout != nil {
|
||||
t := *timeout
|
||||
if t == 0 {
|
||||
return 0
|
||||
}
|
||||
return t + 10*time.Second
|
||||
}
|
||||
return c.opt.ReadTimeout
|
||||
}
|
||||
|
||||
// Close closes the client, releasing any open resources.
|
||||
//
|
||||
// It is rare to Close a Client, as the Client is meant to be
|
||||
// long-lived and shared between many goroutines.
|
||||
func (c *baseClient) Close() error {
|
||||
var firstErr error
|
||||
if c.onClose != nil {
|
||||
if err := c.onClose(); err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
if err := c.connPool.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (c *baseClient) getAddr() string {
|
||||
return c.opt.Addr
|
||||
}
|
||||
|
||||
func (c *baseClient) processPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds)
|
||||
}
|
||||
|
||||
func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds)
|
||||
}
|
||||
|
||||
type pipelineProcessor func(context.Context, *pool.Conn, []Cmder) (bool, error)
|
||||
|
||||
func (c *baseClient) generalProcessPipeline(
|
||||
ctx context.Context, cmds []Cmder, p pipelineProcessor,
|
||||
) error {
|
||||
err := c._generalProcessPipeline(ctx, cmds, p)
|
||||
if err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
return cmdsFirstErr(cmds)
|
||||
}
|
||||
|
||||
func (c *baseClient) _generalProcessPipeline(
|
||||
ctx context.Context, cmds []Cmder, p pipelineProcessor,
|
||||
) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var canRetry bool
|
||||
lastErr = c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
||||
var err error
|
||||
canRetry, err = p(ctx, cn, cmds)
|
||||
return err
|
||||
})
|
||||
if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) {
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *baseClient) pipelineProcessCmds(
|
||||
ctx context.Context, cn *pool.Conn, cmds []Cmder,
|
||||
) (bool, error) {
|
||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmds(wr, cmds)
|
||||
})
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
err = cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
return pipelineReadCmds(rd, cmds)
|
||||
})
|
||||
return true, err
|
||||
}
|
||||
|
||||
func pipelineReadCmds(rd *proto.Reader, cmds []Cmder) error {
|
||||
for _, cmd := range cmds {
|
||||
err := cmd.readReply(rd)
|
||||
cmd.SetErr(err)
|
||||
if err != nil && !isRedisError(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *baseClient) txPipelineProcessCmds(
|
||||
ctx context.Context, cn *pool.Conn, cmds []Cmder,
|
||||
) (bool, error) {
|
||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmds(wr, cmds)
|
||||
})
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
err = cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
statusCmd := cmds[0].(*StatusCmd)
|
||||
// Trim multi and exec.
|
||||
cmds = cmds[1 : len(cmds)-1]
|
||||
|
||||
err := txPipelineReadQueued(rd, statusCmd, cmds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return pipelineReadCmds(rd, cmds)
|
||||
})
|
||||
return false, err
|
||||
}
|
||||
|
||||
func wrapMultiExec(ctx context.Context, cmds []Cmder) []Cmder {
|
||||
if len(cmds) == 0 {
|
||||
panic("not reached")
|
||||
}
|
||||
cmdCopy := make([]Cmder, len(cmds)+2)
|
||||
cmdCopy[0] = NewStatusCmd(ctx, "multi")
|
||||
copy(cmdCopy[1:], cmds)
|
||||
cmdCopy[len(cmdCopy)-1] = NewSliceCmd(ctx, "exec")
|
||||
return cmdCopy
|
||||
}
|
||||
|
||||
func txPipelineReadQueued(rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) error {
|
||||
// Parse queued replies.
|
||||
if err := statusCmd.readReply(rd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for range cmds {
|
||||
if err := statusCmd.readReply(rd); err != nil && !isRedisError(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse number of replies.
|
||||
line, err := rd.ReadLine()
|
||||
if err != nil {
|
||||
if err == Nil {
|
||||
err = TxFailedErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
switch line[0] {
|
||||
case proto.ErrorReply:
|
||||
return proto.ParseErrorReply(line)
|
||||
case proto.ArrayReply:
|
||||
// ok
|
||||
default:
|
||||
err := fmt.Errorf("redis: expected '*', but got line %q", line)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Client is a Redis client representing a pool of zero or more
|
||||
// underlying connections. It's safe for concurrent use by multiple
|
||||
// goroutines.
|
||||
type Client struct {
|
||||
*baseClient
|
||||
cmdable
|
||||
hooks
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// NewClient returns a client to the Redis Server specified by Options.
|
||||
func NewClient(opt *Options) *Client {
|
||||
opt.init()
|
||||
|
||||
c := Client{
|
||||
baseClient: newBaseClient(opt, newConnPool(opt)),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
c.cmdable = c.Process
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Client) clone() *Client {
|
||||
clone := *c
|
||||
clone.cmdable = clone.Process
|
||||
clone.hooks.lock()
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (c *Client) WithTimeout(timeout time.Duration) *Client {
|
||||
clone := c.clone()
|
||||
clone.baseClient = c.baseClient.withTimeout(timeout)
|
||||
return clone
|
||||
}
|
||||
|
||||
func (c *Client) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *Client) WithContext(ctx context.Context) *Client {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
clone := c.clone()
|
||||
clone.ctx = ctx
|
||||
return clone
|
||||
}
|
||||
|
||||
func (c *Client) Conn(ctx context.Context) *Conn {
|
||||
return newConn(ctx, c.opt, pool.NewStickyConnPool(c.connPool))
|
||||
}
|
||||
|
||||
// Do creates a Cmd from the args and processes the cmd.
|
||||
func (c *Client) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c.Process(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) Process(ctx context.Context, cmd Cmder) error {
|
||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
||||
}
|
||||
|
||||
func (c *Client) processPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processPipeline(ctx, cmds, c.baseClient.processPipeline)
|
||||
}
|
||||
|
||||
func (c *Client) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processTxPipeline(ctx, cmds, c.baseClient.processTxPipeline)
|
||||
}
|
||||
|
||||
// Options returns read-only Options that were used to create the client.
|
||||
func (c *Client) Options() *Options {
|
||||
return c.opt
|
||||
}
|
||||
|
||||
type PoolStats pool.Stats
|
||||
|
||||
// PoolStats returns connection pool stats.
|
||||
func (c *Client) PoolStats() *PoolStats {
|
||||
stats := c.connPool.Stats()
|
||||
return (*PoolStats)(stats)
|
||||
}
|
||||
|
||||
func (c *Client) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.Pipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
func (c *Client) Pipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processPipeline,
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Client) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.TxPipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
||||
func (c *Client) TxPipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processTxPipeline,
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Client) pubSub() *PubSub {
|
||||
pubsub := &PubSub{
|
||||
opt: c.opt,
|
||||
|
||||
newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) {
|
||||
return c.newConn(ctx)
|
||||
},
|
||||
closeConn: c.connPool.CloseConn,
|
||||
}
|
||||
pubsub.init()
|
||||
return pubsub
|
||||
}
|
||||
|
||||
// Subscribe subscribes the client to the specified channels.
|
||||
// Channels can be omitted to create empty subscription.
|
||||
// Note that this method does not wait on a response from Redis, so the
|
||||
// subscription may not be active immediately. To force the connection to wait,
|
||||
// you may call the Receive() method on the returned *PubSub like so:
|
||||
//
|
||||
// sub := client.Subscribe(queryResp)
|
||||
// iface, err := sub.Receive()
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// // Should be *Subscription, but others are possible if other actions have been
|
||||
// // taken on sub since it was created.
|
||||
// switch iface.(type) {
|
||||
// case *Subscription:
|
||||
// // subscribe succeeded
|
||||
// case *Message:
|
||||
// // received first message
|
||||
// case *Pong:
|
||||
// // pong received
|
||||
// default:
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// ch := sub.Channel()
|
||||
func (c *Client) Subscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
pubsub := c.pubSub()
|
||||
if len(channels) > 0 {
|
||||
_ = pubsub.Subscribe(ctx, channels...)
|
||||
}
|
||||
return pubsub
|
||||
}
|
||||
|
||||
// PSubscribe subscribes the client to the given patterns.
|
||||
// Patterns can be omitted to create empty subscription.
|
||||
func (c *Client) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
pubsub := c.pubSub()
|
||||
if len(channels) > 0 {
|
||||
_ = pubsub.PSubscribe(ctx, channels...)
|
||||
}
|
||||
return pubsub
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type conn struct {
|
||||
baseClient
|
||||
cmdable
|
||||
statefulCmdable
|
||||
hooks // TODO: inherit hooks
|
||||
}
|
||||
|
||||
// Conn represents a single Redis connection rather than a pool of connections.
|
||||
// Prefer running commands from Client unless there is a specific need
|
||||
// for a continuous single Redis connection.
|
||||
type Conn struct {
|
||||
*conn
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func newConn(ctx context.Context, opt *Options, connPool pool.Pooler) *Conn {
|
||||
c := Conn{
|
||||
conn: &conn{
|
||||
baseClient: baseClient{
|
||||
opt: opt,
|
||||
connPool: connPool,
|
||||
},
|
||||
},
|
||||
ctx: ctx,
|
||||
}
|
||||
c.cmdable = c.Process
|
||||
c.statefulCmdable = c.Process
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Conn) Process(ctx context.Context, cmd Cmder) error {
|
||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
||||
}
|
||||
|
||||
func (c *Conn) processPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processPipeline(ctx, cmds, c.baseClient.processPipeline)
|
||||
}
|
||||
|
||||
func (c *Conn) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processTxPipeline(ctx, cmds, c.baseClient.processTxPipeline)
|
||||
}
|
||||
|
||||
func (c *Conn) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.Pipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
func (c *Conn) Pipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processPipeline,
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Conn) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.TxPipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
||||
func (c *Conn) TxPipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processTxPipeline,
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
+13
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [1.6.0](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) (2024-01-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add Max UUID constant ([#149](https://github.com/google/uuid/issues/149)) ([c58770e](https://github.com/google/uuid/commit/c58770eb495f55fe2ced6284f93c5158a62e53e3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* fix typo in version 7 uuid documentation ([#153](https://github.com/google/uuid/issues/153)) ([016b199](https://github.com/google/uuid/commit/016b199544692f745ffc8867b914129ecb47ef06))
|
||||
* Monotonicity in UUIDv7 ([#150](https://github.com/google/uuid/issues/150)) ([a2b2b32](https://github.com/google/uuid/commit/a2b2b32373ff0b1a312b7fdf6d38a977099698a6))
|
||||
|
||||
## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12)
|
||||
|
||||
|
||||
|
||||
+6
@@ -17,6 +17,12 @@ var (
|
||||
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
|
||||
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
|
||||
Nil UUID // empty UUID, all zeros
|
||||
|
||||
// The Max UUID is special form of UUID that is specified to have all 128 bits set to 1.
|
||||
Max = UUID{
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
}
|
||||
)
|
||||
|
||||
// NewHash returns a new UUID derived from the hash of space concatenated with
|
||||
|
||||
+34
-5
@@ -44,7 +44,7 @@ func NewV7FromReader(r io.Reader) (UUID, error) {
|
||||
|
||||
// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6])
|
||||
// uuid[8] already has the right version number (Variant is 10)
|
||||
// see function NewV7 and NewV7FromReader
|
||||
// see function NewV7 and NewV7FromReader
|
||||
func makeV7(uuid []byte) {
|
||||
/*
|
||||
0 1 2 3
|
||||
@@ -52,7 +52,7 @@ func makeV7(uuid []byte) {
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| unix_ts_ms |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| unix_ts_ms | ver | rand_a |
|
||||
| unix_ts_ms | ver | rand_a (12 bit seq) |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|var| rand_b |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
@@ -61,7 +61,7 @@ func makeV7(uuid []byte) {
|
||||
*/
|
||||
_ = uuid[15] // bounds check
|
||||
|
||||
t := timeNow().UnixMilli()
|
||||
t, s := getV7Time()
|
||||
|
||||
uuid[0] = byte(t >> 40)
|
||||
uuid[1] = byte(t >> 32)
|
||||
@@ -70,6 +70,35 @@ func makeV7(uuid []byte) {
|
||||
uuid[4] = byte(t >> 8)
|
||||
uuid[5] = byte(t)
|
||||
|
||||
uuid[6] = 0x70 | (uuid[6] & 0x0F)
|
||||
// uuid[8] has already has right version
|
||||
uuid[6] = 0x70 | (0x0F & byte(s>>8))
|
||||
uuid[7] = byte(s)
|
||||
}
|
||||
|
||||
// lastV7time is the last time we returned stored as:
|
||||
//
|
||||
// 52 bits of time in milliseconds since epoch
|
||||
// 12 bits of (fractional nanoseconds) >> 8
|
||||
var lastV7time int64
|
||||
|
||||
const nanoPerMilli = 1000000
|
||||
|
||||
// getV7Time returns the time in milliseconds and nanoseconds / 256.
|
||||
// The returned (milli << 12 + seq) is guarenteed to be greater than
|
||||
// (milli << 12 + seq) returned by any previous call to getV7Time.
|
||||
func getV7Time() (milli, seq int64) {
|
||||
timeMu.Lock()
|
||||
defer timeMu.Unlock()
|
||||
|
||||
nano := timeNow().UnixNano()
|
||||
milli = nano / nanoPerMilli
|
||||
// Sequence number is between 0 and 3906 (nanoPerMilli>>8)
|
||||
seq = (nano - milli*nanoPerMilli) >> 8
|
||||
now := milli<<12 + seq
|
||||
if now <= lastV7time {
|
||||
now = lastV7time + 1
|
||||
milli = now >> 12
|
||||
seq = now & 0xfff
|
||||
}
|
||||
lastV7time = now
|
||||
return milli, seq
|
||||
}
|
||||
|
||||
-91
@@ -1,91 +0,0 @@
|
||||
# IdentIcon
|
||||
|
||||
|
||||

|
||||
[](https://goreportcard.com/report/github.com/nullrocks/identicon) [](http://godoc.org/github.com/nullrocks/identicon)
|
||||
|
||||
**IdentIcon** is an open source avatar generator inspired by GitHub avatars.
|
||||
|
||||
|
||||
IdentIcon uses a deterministic algorithm that generates an image (using Golang's stdlib image encoders) based on a text (Generally Usernames, Emails or just random strings), by hashing it and iterating over the bytes of the digest to pick whether to draw a point, pick a color or choose where to go next.
|
||||
|
||||
|
||||
IdentIcon's Generator enables the creation of customized figures: (NxN size, points density, custom color palette) as well as multiple exporting formats in case the developers want to generate their own images.
|
||||
|
||||
## Installation
|
||||
```bash
|
||||
$ go get github.com/nullrocks/identicon
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/nullrocks/identicon"
|
||||
)
|
||||
|
||||
// New Generator: Rehuse
|
||||
ig, err := identicon.New(
|
||||
"github", // Namespace
|
||||
5, // Number of blocks (Size)
|
||||
3, // Density
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(err) // Invalid Size or Density
|
||||
}
|
||||
|
||||
username := "nullrocks" // Text - decides the resulting figure
|
||||
ii, err := ig.Draw(username) // Generate an IdentIcon
|
||||
|
||||
if err != nil {
|
||||
panic(err) // Text is empty
|
||||
}
|
||||
|
||||
// File writer
|
||||
img, _ := os.Create("icon.png")
|
||||
defer img.Close()
|
||||
// Takes the size in pixels and any io.Writer
|
||||
ii.Png(300, img) // 300px * 300px
|
||||
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### 5x5
|
||||
|nullrocks | johndoe | abc123 | modulo |
|
||||
:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:--------------------------------------------|
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
|
||||
### 7x7
|
||||
|nullrocks | johndoe | abc123 | modulo |
|
||||
:------------------------------------------------:|:---------------------------------------------:|:-------------------------------------------:|:---------------------------------------------|
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
|
||||
### 10x10
|
||||
|nullrocks | johndoe | abc123 | modulo |
|
||||
:--------------------------------------------------:|:-----------------------------------------------:|:---------------------------------------------:|:----------------------------------------------|
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
 |  |  |  |
|
||||
|
||||
[View examples](./examples)
|
||||
|
||||
## Documentation
|
||||
|
||||
## Changelog
|
||||
|
||||
## Contribution
|
||||
|
||||
## License
|
||||
MIT
|
||||
|
||||
Copyright (c) 2018-present, Ruben Rivera
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
*.rdb
|
||||
testdata/*
|
||||
.idea/
|
||||
.DS_Store
|
||||
*.tar.gz
|
||||
*.dic
|
||||
Generated
Vendored
Generated
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
## [9.0.5](https://github.com/redis/go-redis/compare/v9.0.4...v9.0.5) (2023-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add ACL LOG ([#2536](https://github.com/redis/go-redis/issues/2536)) ([31ba855](https://github.com/redis/go-redis/commit/31ba855ddebc38fbcc69a75d9d4fb769417cf602))
|
||||
* add field protocol to setupClusterQueryParams ([#2600](https://github.com/redis/go-redis/issues/2600)) ([840c25c](https://github.com/redis/go-redis/commit/840c25cb6f320501886a82a5e75f47b491e46fbe))
|
||||
* add protocol option ([#2598](https://github.com/redis/go-redis/issues/2598)) ([3917988](https://github.com/redis/go-redis/commit/391798880cfb915c4660f6c3ba63e0c1a459e2af))
|
||||
|
||||
|
||||
|
||||
## [9.0.4](https://github.com/redis/go-redis/compare/v9.0.3...v9.0.4) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* reader float parser ([#2513](https://github.com/redis/go-redis/issues/2513)) ([46f2450](https://github.com/redis/go-redis/commit/46f245075e6e3a8bd8471f9ca67ea95fd675e241))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add client info command ([#2483](https://github.com/redis/go-redis/issues/2483)) ([b8c7317](https://github.com/redis/go-redis/commit/b8c7317cc6af444603731f7017c602347c0ba61e))
|
||||
* no longer verify HELLO error messages ([#2515](https://github.com/redis/go-redis/issues/2515)) ([7b4f217](https://github.com/redis/go-redis/commit/7b4f2179cb5dba3d3c6b0c6f10db52b837c912c8))
|
||||
* read the structure to increase the judgment of the omitempty op… ([#2529](https://github.com/redis/go-redis/issues/2529)) ([37c057b](https://github.com/redis/go-redis/commit/37c057b8e597c5e8a0e372337f6a8ad27f6030af))
|
||||
|
||||
|
||||
|
||||
## [9.0.3](https://github.com/redis/go-redis/compare/v9.0.2...v9.0.3) (2023-04-02)
|
||||
|
||||
### New Features
|
||||
|
||||
- feat(scan): scan time.Time sets the default decoding (#2413)
|
||||
- Add support for CLUSTER LINKS command (#2504)
|
||||
- Add support for acl dryrun command (#2502)
|
||||
- Add support for COMMAND GETKEYS & COMMAND GETKEYSANDFLAGS (#2500)
|
||||
- Add support for LCS Command (#2480)
|
||||
- Add support for BZMPOP (#2456)
|
||||
- Adding support for ZMPOP command (#2408)
|
||||
- Add support for LMPOP (#2440)
|
||||
- feat: remove pool unused fields (#2438)
|
||||
- Expiretime and PExpireTime (#2426)
|
||||
- Implement `FUNCTION` group of commands (#2475)
|
||||
- feat(zadd): add ZAddLT and ZAddGT (#2429)
|
||||
- Add: Support for COMMAND LIST command (#2491)
|
||||
- Add support for BLMPOP (#2442)
|
||||
- feat: check pipeline.Do to prevent confusion with Exec (#2517)
|
||||
- Function stats, function kill, fcall and fcall_ro (#2486)
|
||||
- feat: Add support for CLUSTER SHARDS command (#2507)
|
||||
- feat(cmd): support for adding byte,bit parameters to the bitpos command (#2498)
|
||||
|
||||
### Fixed
|
||||
|
||||
- fix: eval api cmd.SetFirstKeyPos (#2501)
|
||||
- fix: limit the number of connections created (#2441)
|
||||
- fixed #2462 v9 continue support dragonfly, it's Hello command return "NOAUTH Authentication required" error (#2479)
|
||||
- Fix for internal/hscan/structmap.go:89:23: undefined: reflect.Pointer (#2458)
|
||||
- fix: group lag can be null (#2448)
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Updating to the latest version of redis (#2508)
|
||||
- Allowing for running tests on a port other than the fixed 6380 (#2466)
|
||||
- redis 7.0.8 in tests (#2450)
|
||||
- docs: Update redisotel example for v9 (#2425)
|
||||
- chore: update go mod, Upgrade golang.org/x/net version to 0.7.0 (#2476)
|
||||
- chore: add Chinese translation (#2436)
|
||||
- chore(deps): bump github.com/bsm/gomega from 1.20.0 to 1.26.0 (#2421)
|
||||
- chore(deps): bump github.com/bsm/ginkgo/v2 from 2.5.0 to 2.7.0 (#2420)
|
||||
- chore(deps): bump actions/setup-go from 3 to 4 (#2495)
|
||||
- docs: add instructions for the HSet api (#2503)
|
||||
- docs: add reading lag field comment (#2451)
|
||||
- test: update go mod before testing(go mod tidy) (#2423)
|
||||
- docs: fix comment typo (#2505)
|
||||
- test: remove testify (#2463)
|
||||
- refactor: change ListElementCmd to KeyValuesCmd. (#2443)
|
||||
- fix(appendArg): appendArg case special type (#2489)
|
||||
|
||||
## [9.0.2](https://github.com/redis/go-redis/compare/v9.0.1...v9.0.2) (2023-02-01)
|
||||
|
||||
### Features
|
||||
|
||||
* upgrade OpenTelemetry, use the new metrics API. ([#2410](https://github.com/redis/go-redis/issues/2410)) ([e29e42c](https://github.com/redis/go-redis/commit/e29e42cde2755ab910d04185025dc43ce6f59c65))
|
||||
|
||||
## v9 2023-01-30
|
||||
|
||||
### Breaking
|
||||
|
||||
- Changed Pipelines to not be thread-safe any more.
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for [RESP3](https://github.com/antirez/RESP3/blob/master/spec.md) protocol. It was
|
||||
contributed by @monkey92t who has done the majority of work in this release.
|
||||
- Added `ContextTimeoutEnabled` option that controls whether the client respects context timeouts
|
||||
and deadlines. See
|
||||
[Redis Timeouts](https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts) for details.
|
||||
- Added `ParseClusterURL` to parse URLs into `ClusterOptions`, for example,
|
||||
`redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791`.
|
||||
- Added metrics instrumentation using `redisotel.IstrumentMetrics`. See
|
||||
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html)
|
||||
- Added `redis.HasErrorPrefix` to help working with errors.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed asynchronous cancellation based on the context timeout. It was racy in v8 and is
|
||||
completely gone in v9.
|
||||
- Reworked hook interface and added `DialHook`.
|
||||
- Replaced `redisotel.NewTracingHook` with `redisotel.InstrumentTracing`. See
|
||||
[example](example/otel) and
|
||||
[documentation](https://redis.uptrace.dev/guide/go-redis-monitoring.html).
|
||||
- Replaced `*redis.Z` with `redis.Z` since it is small enough to be passed as value without making
|
||||
an allocation.
|
||||
- Renamed the option `MaxConnAge` to `ConnMaxLifetime`.
|
||||
- Renamed the option `IdleTimeout` to `ConnMaxIdleTime`.
|
||||
- Removed connection reaper in favor of `MaxIdleConns`.
|
||||
- Removed `WithContext` since `context.Context` can be passed directly as an arg.
|
||||
- Removed `Pipeline.Close` since there is no real need to explicitly manage pipeline resources and
|
||||
it can be safely reused via `sync.Pool` etc. `Pipeline.Discard` is still available if you want to
|
||||
reset commands for some reason.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved and fixed pipeline retries.
|
||||
- As usually, added support for more commands and fixed some bugs.
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# Contributing
|
||||
|
||||
## Introduction
|
||||
|
||||
We appreciate your interest in considering contributing to go-redis.
|
||||
Community contributions mean a lot to us.
|
||||
|
||||
## Contributions we need
|
||||
|
||||
You may already know how you'd like to contribute, whether it's a fix for a bug you
|
||||
encountered, or a new feature your team wants to use.
|
||||
|
||||
If you don't know where to start, consider improving
|
||||
documentation, bug triaging, and writing tutorials are all examples of
|
||||
helpful contributions that mean less work for you.
|
||||
|
||||
## Your First Contribution
|
||||
|
||||
Unsure where to begin contributing? You can start by looking through
|
||||
[help-wanted
|
||||
issues](https://github.com/redis/go-redis/issues?q=is%3Aopen+is%3Aissue+label%3ahelp-wanted).
|
||||
|
||||
Never contributed to open source before? Here are a couple of friendly
|
||||
tutorials:
|
||||
|
||||
- <http://makeapullrequest.com/>
|
||||
- <http://www.firsttimersonly.com/>
|
||||
|
||||
## Getting Started
|
||||
|
||||
Here's how to get started with your code contribution:
|
||||
|
||||
1. Create your own fork of go-redis
|
||||
2. Do the changes in your fork
|
||||
3. If you need a development environment, run `make test`. Note: this clones and builds the latest release of [redis](https://redis.io). You also need a redis-stack-server docker, in order to run the capabilities tests. This can be started by running:
|
||||
```docker run -p 6379:6379 -it redis/redis-stack-server:edge```
|
||||
4. While developing, make sure the tests pass by running `make tests`
|
||||
5. If you like the change and think the project could use it, send a
|
||||
pull request
|
||||
|
||||
To see what else is part of the automation, run `invoke -l`
|
||||
|
||||
## Testing
|
||||
|
||||
Call `make test` to run all tests, including linters.
|
||||
|
||||
Continuous Integration uses these same wrappers to run all of these
|
||||
tests against multiple versions of python. Feel free to test your
|
||||
changes against all the go versions supported, as declared by the
|
||||
[build.yml](./.github/workflows/build.yml) file.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
If you get any errors when running `make test`, make sure
|
||||
that you are using supported versions of Docker and go.
|
||||
|
||||
## How to Report a Bug
|
||||
|
||||
### Security Vulnerabilities
|
||||
|
||||
**NOTE**: If you find a security vulnerability, do NOT open an issue.
|
||||
Email [Redis Open Source (<oss@redis.com>)](mailto:oss@redis.com) instead.
|
||||
|
||||
In order to determine whether you are dealing with a security issue, ask
|
||||
yourself these two questions:
|
||||
|
||||
- Can I access something that's not mine, or something I shouldn't
|
||||
have access to?
|
||||
- Can I disable something for other people?
|
||||
|
||||
If the answer to either of those two questions are *yes*, then you're
|
||||
probably dealing with a security issue. Note that even if you answer
|
||||
*no* to both questions, you may still be dealing with a security
|
||||
issue, so if you're unsure, just email [us](mailto:oss@redis.com).
|
||||
|
||||
### Everything Else
|
||||
|
||||
When filing an issue, make sure to answer these five questions:
|
||||
|
||||
1. What version of go-redis are you using?
|
||||
2. What version of redis are you using?
|
||||
3. What did you do?
|
||||
4. What did you expect to see?
|
||||
5. What did you see instead?
|
||||
|
||||
## Suggest a feature or enhancement
|
||||
|
||||
If you'd like to contribute a new feature, make sure you check our
|
||||
issue list to see if someone has already proposed it. Work may already
|
||||
be underway on the feature you want or we may have rejected a
|
||||
feature like it already.
|
||||
|
||||
If you don't see anything, open a new issue that describes the feature
|
||||
you would like and how it should work.
|
||||
|
||||
## Code review process
|
||||
|
||||
The core team regularly looks at pull requests. We will provide
|
||||
feedback as soon as possible. After receiving our feedback, please respond
|
||||
within two weeks. After that time, we may close your PR if it isn't
|
||||
showing any activity.
|
||||
Generated
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2013 The github.com/go-redis/redis Authors.
|
||||
Copyright (c) 2013 The github.com/redis/go-redis Authors.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort)
|
||||
|
||||
test: testdeps
|
||||
set -e; for dir in $(GO_MOD_DIRS); do \
|
||||
echo "go test in $${dir}"; \
|
||||
(cd "$${dir}" && \
|
||||
go mod tidy -compat=1.18 && \
|
||||
go test && \
|
||||
go test ./... -short -race && \
|
||||
go test ./... -run=NONE -bench=. -benchmem && \
|
||||
env GOOS=linux GOARCH=386 go test && \
|
||||
go vet); \
|
||||
done
|
||||
cd internal/customvet && go build .
|
||||
go vet -vettool ./internal/customvet/customvet
|
||||
|
||||
testdeps: testdata/redis/src/redis-server
|
||||
|
||||
bench: testdeps
|
||||
go test ./... -test.run=NONE -test.bench=. -test.benchmem
|
||||
|
||||
.PHONY: all test testdeps bench fmt
|
||||
|
||||
build:
|
||||
go build .
|
||||
|
||||
testdata/redis:
|
||||
mkdir -p $@
|
||||
wget -qO- https://download.redis.io/releases/redis-7.2.1.tar.gz | tar xvz --strip-components=1 -C $@
|
||||
|
||||
testdata/redis/src/redis-server: testdata/redis
|
||||
cd $< && make all
|
||||
|
||||
fmt:
|
||||
gofumpt -w ./
|
||||
goimports -w -local github.com/redis/go-redis ./
|
||||
|
||||
go_mod_tidy:
|
||||
set -e; for dir in $(GO_MOD_DIRS); do \
|
||||
echo "go mod tidy in $${dir}"; \
|
||||
(cd "$${dir}" && \
|
||||
go get -u ./... && \
|
||||
go mod tidy -compat=1.18); \
|
||||
done
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
# Redis client for Go
|
||||
|
||||
[](https://github.com/redis/go-redis/actions)
|
||||
[](https://pkg.go.dev/github.com/redis/go-redis/v9?tab=doc)
|
||||
[](https://redis.uptrace.dev/)
|
||||
[](https://discord.gg/rWtp5Aj)
|
||||
|
||||
> go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
|
||||
> Uptrace is an open-source APM tool that supports distributed tracing, metrics, and logs. You can
|
||||
> use it to monitor applications and set up automatic alerts to receive notifications via email,
|
||||
> Slack, Telegram, and others.
|
||||
>
|
||||
> See [OpenTelemetry](https://github.com/redis/go-redis/tree/master/example/otel) example which
|
||||
> demonstrates how you can use Uptrace to monitor go-redis.
|
||||
|
||||
## How do I Redis?
|
||||
|
||||
[Learn for free at Redis University](https://university.redis.com/)
|
||||
|
||||
[Build faster with the Redis Launchpad](https://launchpad.redis.com/)
|
||||
|
||||
[Try the Redis Cloud](https://redis.com/try-free/)
|
||||
|
||||
[Dive in developer tutorials](https://developer.redis.com/)
|
||||
|
||||
[Join the Redis community](https://redis.com/community/)
|
||||
|
||||
[Work at Redis](https://redis.com/company/careers/jobs/)
|
||||
|
||||
## Documentation
|
||||
|
||||
- [English](https://redis.uptrace.dev)
|
||||
- [简体中文](https://redis.uptrace.dev/zh/)
|
||||
|
||||
## Resources
|
||||
|
||||
- [Discussions](https://github.com/redis/go-redis/discussions)
|
||||
- [Chat](https://discord.gg/rWtp5Aj)
|
||||
- [Reference](https://pkg.go.dev/github.com/redis/go-redis/v9)
|
||||
- [Examples](https://pkg.go.dev/github.com/redis/go-redis/v9#pkg-examples)
|
||||
|
||||
## Ecosystem
|
||||
|
||||
- [Redis Mock](https://github.com/go-redis/redismock)
|
||||
- [Distributed Locks](https://github.com/bsm/redislock)
|
||||
- [Redis Cache](https://github.com/go-redis/cache)
|
||||
- [Rate limiting](https://github.com/go-redis/redis_rate)
|
||||
|
||||
This client also works with [Kvrocks](https://github.com/apache/incubator-kvrocks), a distributed
|
||||
key value NoSQL database that uses RocksDB as storage engine and is compatible with Redis protocol.
|
||||
|
||||
## Features
|
||||
|
||||
- Redis 3 commands except QUIT, MONITOR, and SYNC.
|
||||
- Automatic connection pooling with
|
||||
- [Pub/Sub](https://redis.uptrace.dev/guide/go-redis-pubsub.html).
|
||||
- [Pipelines and transactions](https://redis.uptrace.dev/guide/go-redis-pipelines.html).
|
||||
- [Scripting](https://redis.uptrace.dev/guide/lua-scripting.html).
|
||||
- [Redis Sentinel](https://redis.uptrace.dev/guide/go-redis-sentinel.html).
|
||||
- [Redis Cluster](https://redis.uptrace.dev/guide/go-redis-cluster.html).
|
||||
- [Redis Ring](https://redis.uptrace.dev/guide/ring.html).
|
||||
- [Redis Performance Monitoring](https://redis.uptrace.dev/guide/redis-performance-monitoring.html).
|
||||
- [Redis Probabilistic [RedisStack]](https://redis.io/docs/data-types/probabilistic/)
|
||||
|
||||
## Installation
|
||||
|
||||
go-redis supports 2 last Go versions and requires a Go version with
|
||||
[modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go
|
||||
module:
|
||||
|
||||
```shell
|
||||
go mod init github.com/my/repo
|
||||
```
|
||||
|
||||
Then install go-redis/**v9**:
|
||||
|
||||
```shell
|
||||
go get github.com/redis/go-redis/v9
|
||||
```
|
||||
|
||||
## Quickstart
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
func ExampleClient() {
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
})
|
||||
|
||||
err := rdb.Set(ctx, "key", "value", 0).Err()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
val, err := rdb.Get(ctx, "key").Result()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fmt.Println("key", val)
|
||||
|
||||
val2, err := rdb.Get(ctx, "key2").Result()
|
||||
if err == redis.Nil {
|
||||
fmt.Println("key2 does not exist")
|
||||
} else if err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
fmt.Println("key2", val2)
|
||||
}
|
||||
// Output: key value
|
||||
// key2 does not exist
|
||||
}
|
||||
```
|
||||
|
||||
The above can be modified to specify the version of the RESP protocol by adding the `protocol`
|
||||
option to the `Options` struct:
|
||||
|
||||
```go
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: "localhost:6379",
|
||||
Password: "", // no password set
|
||||
DB: 0, // use default DB
|
||||
Protocol: 3, // specify 2 for RESP 2 or 3 for RESP 3
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
### Connecting via a redis url
|
||||
|
||||
go-redis also supports connecting via the
|
||||
[redis uri specification](https://github.com/redis/redis-specifications/tree/master/uri/redis.txt).
|
||||
The example below demonstrates how the connection can easily be configured using a string, adhering
|
||||
to this specification.
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
func ExampleClient() *redis.Client {
|
||||
url := "redis://user:password@localhost:6379/0?protocol=3"
|
||||
opts, err := redis.ParseURL(url)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return redis.NewClient(opts)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please see [out contributing guidelines](CONTRIBUTING.md) to help us improve this library!
|
||||
|
||||
## Look and feel
|
||||
|
||||
Some corner cases:
|
||||
|
||||
```go
|
||||
// SET key value EX 10 NX
|
||||
set, err := rdb.SetNX(ctx, "key", "value", 10*time.Second).Result()
|
||||
|
||||
// SET key value keepttl NX
|
||||
set, err := rdb.SetNX(ctx, "key", "value", redis.KeepTTL).Result()
|
||||
|
||||
// SORT list LIMIT 0 2 ASC
|
||||
vals, err := rdb.Sort(ctx, "list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result()
|
||||
|
||||
// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2
|
||||
vals, err := rdb.ZRangeByScoreWithScores(ctx, "zset", &redis.ZRangeBy{
|
||||
Min: "-inf",
|
||||
Max: "+inf",
|
||||
Offset: 0,
|
||||
Count: 2,
|
||||
}).Result()
|
||||
|
||||
// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM
|
||||
vals, err := rdb.ZInterStore(ctx, "out", &redis.ZStore{
|
||||
Keys: []string{"zset1", "zset2"},
|
||||
Weights: []int64{2, 3}
|
||||
}).Result()
|
||||
|
||||
// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello"
|
||||
vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result()
|
||||
|
||||
// custom command
|
||||
res, err := rdb.Do(ctx, "set", "key", "value").Result()
|
||||
```
|
||||
|
||||
## Run the test
|
||||
|
||||
go-redis will start a redis-server and run the test cases.
|
||||
|
||||
The paths of redis-server bin file and redis config file are defined in `main_test.go`:
|
||||
|
||||
```go
|
||||
var (
|
||||
redisServerBin, _ = filepath.Abs(filepath.Join("testdata", "redis", "src", "redis-server"))
|
||||
redisServerConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "redis.conf"))
|
||||
)
|
||||
```
|
||||
|
||||
For local testing, you can change the variables to refer to your local files, or create a soft link
|
||||
to the corresponding folder for redis-server and copy the config file to `testdata/redis/`:
|
||||
|
||||
```shell
|
||||
ln -s /usr/bin/redis-server ./go-redis/testdata/redis/src
|
||||
cp ./go-redis/testdata/redis.conf ./go-redis/testdata/redis/
|
||||
```
|
||||
|
||||
Lastly, run:
|
||||
|
||||
```shell
|
||||
go test
|
||||
```
|
||||
|
||||
Another option is to run your specific tests with an already running redis. The example below, tests
|
||||
against a redis running on port 9999.:
|
||||
|
||||
```shell
|
||||
REDIS_PORT=9999 go test <your options>
|
||||
```
|
||||
|
||||
## See also
|
||||
|
||||
- [Golang ORM](https://bun.uptrace.dev) for PostgreSQL, MySQL, MSSQL, and SQLite
|
||||
- [Golang PostgreSQL](https://bun.uptrace.dev/postgres/)
|
||||
- [Golang HTTP router](https://bunrouter.uptrace.dev/)
|
||||
- [Golang ClickHouse ORM](https://github.com/uptrace/go-clickhouse)
|
||||
|
||||
## Contributors
|
||||
|
||||
Thanks to all the people who already contributed!
|
||||
|
||||
<a href="https://github.com/redis/go-redis/graphs/contributors">
|
||||
<img src="https://contributors-img.web.app/image?repo=redis/go-redis" />
|
||||
</a>
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
type ACLCmdable interface {
|
||||
ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd
|
||||
ACLLog(ctx context.Context, count int64) *ACLLogCmd
|
||||
ACLLogReset(ctx context.Context) *StatusCmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLDryRun(ctx context.Context, username string, command ...interface{}) *StringCmd {
|
||||
args := make([]interface{}, 0, 3+len(command))
|
||||
args = append(args, "acl", "dryrun", username)
|
||||
args = append(args, command...)
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLLog(ctx context.Context, count int64) *ACLLogCmd {
|
||||
args := make([]interface{}, 0, 3)
|
||||
args = append(args, "acl", "log")
|
||||
if count > 0 {
|
||||
args = append(args, count)
|
||||
}
|
||||
cmd := NewACLLogCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ACLLogReset(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "acl", "log", "reset")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type BitMapCmdable interface {
|
||||
GetBit(ctx context.Context, key string, offset int64) *IntCmd
|
||||
SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd
|
||||
BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd
|
||||
BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd
|
||||
BitOpNot(ctx context.Context, destKey string, key string) *IntCmd
|
||||
BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd
|
||||
BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd
|
||||
BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd
|
||||
}
|
||||
|
||||
func (c cmdable) GetBit(ctx context.Context, key string, offset int64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "getbit", key, offset)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SetBit(ctx context.Context, key string, offset int64, value int) *IntCmd {
|
||||
cmd := NewIntCmd(
|
||||
ctx,
|
||||
"setbit",
|
||||
key,
|
||||
offset,
|
||||
value,
|
||||
)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type BitCount struct {
|
||||
Start, End int64
|
||||
}
|
||||
|
||||
func (c cmdable) BitCount(ctx context.Context, key string, bitCount *BitCount) *IntCmd {
|
||||
args := []interface{}{"bitcount", key}
|
||||
if bitCount != nil {
|
||||
args = append(
|
||||
args,
|
||||
bitCount.Start,
|
||||
bitCount.End,
|
||||
)
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) bitOp(ctx context.Context, op, destKey string, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 3+len(keys))
|
||||
args[0] = "bitop"
|
||||
args[1] = op
|
||||
args[2] = destKey
|
||||
for i, key := range keys {
|
||||
args[3+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) BitOpAnd(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "and", destKey, keys...)
|
||||
}
|
||||
|
||||
func (c cmdable) BitOpOr(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "or", destKey, keys...)
|
||||
}
|
||||
|
||||
func (c cmdable) BitOpXor(ctx context.Context, destKey string, keys ...string) *IntCmd {
|
||||
return c.bitOp(ctx, "xor", destKey, keys...)
|
||||
}
|
||||
|
||||
func (c cmdable) BitOpNot(ctx context.Context, destKey string, key string) *IntCmd {
|
||||
return c.bitOp(ctx, "not", destKey, key)
|
||||
}
|
||||
|
||||
// BitPos is an API before Redis version 7.0, cmd: bitpos key bit start end
|
||||
// if you need the `byte | bit` parameter, please use `BitPosSpan`.
|
||||
func (c cmdable) BitPos(ctx context.Context, key string, bit int64, pos ...int64) *IntCmd {
|
||||
args := make([]interface{}, 3+len(pos))
|
||||
args[0] = "bitpos"
|
||||
args[1] = key
|
||||
args[2] = bit
|
||||
switch len(pos) {
|
||||
case 0:
|
||||
case 1:
|
||||
args[3] = pos[0]
|
||||
case 2:
|
||||
args[3] = pos[0]
|
||||
args[4] = pos[1]
|
||||
default:
|
||||
panic("too many arguments")
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// BitPosSpan supports the `byte | bit` parameters in redis version 7.0,
|
||||
// the bitpos command defaults to using byte type for the `start-end` range,
|
||||
// which means it counts in bytes from start to end. you can set the value
|
||||
// of "span" to determine the type of `start-end`.
|
||||
// span = "bit", cmd: bitpos key bit start end bit
|
||||
// span = "byte", cmd: bitpos key bit start end byte
|
||||
func (c cmdable) BitPosSpan(ctx context.Context, key string, bit int8, start, end int64, span string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "bitpos", key, bit, start, end, span)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// BitField accepts multiple values:
|
||||
// - BitField("set", "i1", "offset1", "value1","cmd2", "type2", "offset2", "value2")
|
||||
// - BitField([]string{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"})
|
||||
// - BitField([]interface{}{"cmd1", "type1", "offset1", "value1","cmd2", "type2", "offset2", "value2"})
|
||||
func (c cmdable) BitField(ctx context.Context, key string, values ...interface{}) *IntSliceCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "bitfield"
|
||||
args[1] = key
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// BitFieldRO - Read-only variant of the BITFIELD command.
|
||||
// It is like the original BITFIELD but only accepts GET subcommand and can safely be used in read-only replicas.
|
||||
// - BitFieldRO(ctx, key, "<Encoding0>", "<Offset0>", "<Encoding1>","<Offset1>")
|
||||
func (c cmdable) BitFieldRO(ctx context.Context, key string, values ...interface{}) *IntSliceCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "BITFIELD_RO"
|
||||
args[1] = key
|
||||
if len(values)%2 != 0 {
|
||||
panic("BitFieldRO: invalid number of arguments, must be even")
|
||||
}
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
args = append(args, "GET", values[i], values[i+1])
|
||||
}
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
type ClusterCmdable interface {
|
||||
ClusterMyShardID(ctx context.Context) *StringCmd
|
||||
ClusterSlots(ctx context.Context) *ClusterSlotsCmd
|
||||
ClusterShards(ctx context.Context) *ClusterShardsCmd
|
||||
ClusterLinks(ctx context.Context) *ClusterLinksCmd
|
||||
ClusterNodes(ctx context.Context) *StringCmd
|
||||
ClusterMeet(ctx context.Context, host, port string) *StatusCmd
|
||||
ClusterForget(ctx context.Context, nodeID string) *StatusCmd
|
||||
ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd
|
||||
ClusterResetSoft(ctx context.Context) *StatusCmd
|
||||
ClusterResetHard(ctx context.Context) *StatusCmd
|
||||
ClusterInfo(ctx context.Context) *StringCmd
|
||||
ClusterKeySlot(ctx context.Context, key string) *IntCmd
|
||||
ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd
|
||||
ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd
|
||||
ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd
|
||||
ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd
|
||||
ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd
|
||||
ClusterSaveConfig(ctx context.Context) *StatusCmd
|
||||
ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd
|
||||
ClusterFailover(ctx context.Context) *StatusCmd
|
||||
ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd
|
||||
ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd
|
||||
ReadOnly(ctx context.Context) *StatusCmd
|
||||
ReadWrite(ctx context.Context) *StatusCmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterMyShardID(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "cluster", "myshardid")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterSlots(ctx context.Context) *ClusterSlotsCmd {
|
||||
cmd := NewClusterSlotsCmd(ctx, "cluster", "slots")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterShards(ctx context.Context) *ClusterShardsCmd {
|
||||
cmd := NewClusterShardsCmd(ctx, "cluster", "shards")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterLinks(ctx context.Context) *ClusterLinksCmd {
|
||||
cmd := NewClusterLinksCmd(ctx, "cluster", "links")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterNodes(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "cluster", "nodes")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterMeet(ctx context.Context, host, port string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "cluster", "meet", host, port)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterForget(ctx context.Context, nodeID string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "cluster", "forget", nodeID)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterReplicate(ctx context.Context, nodeID string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "cluster", "replicate", nodeID)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterResetSoft(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "cluster", "reset", "soft")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterResetHard(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "cluster", "reset", "hard")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterInfo(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "cluster", "info")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterKeySlot(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "cluster", "keyslot", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterGetKeysInSlot(ctx context.Context, slot int, count int) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "cluster", "getkeysinslot", slot, count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterCountFailureReports(ctx context.Context, nodeID string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "cluster", "count-failure-reports", nodeID)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterCountKeysInSlot(ctx context.Context, slot int) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "cluster", "countkeysinslot", slot)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterDelSlots(ctx context.Context, slots ...int) *StatusCmd {
|
||||
args := make([]interface{}, 2+len(slots))
|
||||
args[0] = "cluster"
|
||||
args[1] = "delslots"
|
||||
for i, slot := range slots {
|
||||
args[2+i] = slot
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterDelSlotsRange(ctx context.Context, min, max int) *StatusCmd {
|
||||
size := max - min + 1
|
||||
slots := make([]int, size)
|
||||
for i := 0; i < size; i++ {
|
||||
slots[i] = min + i
|
||||
}
|
||||
return c.ClusterDelSlots(ctx, slots...)
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterSaveConfig(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "cluster", "saveconfig")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterSlaves(ctx context.Context, nodeID string) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "cluster", "slaves", nodeID)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterFailover(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "cluster", "failover")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterAddSlots(ctx context.Context, slots ...int) *StatusCmd {
|
||||
args := make([]interface{}, 2+len(slots))
|
||||
args[0] = "cluster"
|
||||
args[1] = "addslots"
|
||||
for i, num := range slots {
|
||||
args[2+i] = num
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClusterAddSlotsRange(ctx context.Context, min, max int) *StatusCmd {
|
||||
size := max - min + 1
|
||||
slots := make([]int, size)
|
||||
for i := 0; i < size; i++ {
|
||||
slots[i] = min + i
|
||||
}
|
||||
return c.ClusterAddSlots(ctx, slots...)
|
||||
}
|
||||
|
||||
func (c cmdable) ReadOnly(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "readonly")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ReadWrite(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "readwrite")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+5473
File diff suppressed because it is too large
Load Diff
+718
@@ -0,0 +1,718 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
)
|
||||
|
||||
// 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.
|
||||
// For example:
|
||||
//
|
||||
// rdb.Set(ctx, key, value, redis.KeepTTL)
|
||||
const KeepTTL = -1
|
||||
|
||||
func usePrecise(dur time.Duration) bool {
|
||||
return dur < time.Second || dur%time.Second != 0
|
||||
}
|
||||
|
||||
func formatMs(ctx context.Context, dur time.Duration) int64 {
|
||||
if dur > 0 && dur < time.Millisecond {
|
||||
internal.Logger.Printf(
|
||||
ctx,
|
||||
"specified duration is %s, but minimal supported value is %s - truncating to 1ms",
|
||||
dur, time.Millisecond,
|
||||
)
|
||||
return 1
|
||||
}
|
||||
return int64(dur / time.Millisecond)
|
||||
}
|
||||
|
||||
func formatSec(ctx context.Context, dur time.Duration) int64 {
|
||||
if dur > 0 && dur < time.Second {
|
||||
internal.Logger.Printf(
|
||||
ctx,
|
||||
"specified duration is %s, but minimal supported value is %s - truncating to 1s",
|
||||
dur, time.Second,
|
||||
)
|
||||
return 1
|
||||
}
|
||||
return int64(dur / time.Second)
|
||||
}
|
||||
|
||||
func appendArgs(dst, src []interface{}) []interface{} {
|
||||
if len(src) == 1 {
|
||||
return appendArg(dst, src[0])
|
||||
}
|
||||
|
||||
dst = append(dst, src...)
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendArg(dst []interface{}, arg interface{}) []interface{} {
|
||||
switch arg := arg.(type) {
|
||||
case []string:
|
||||
for _, s := range arg {
|
||||
dst = append(dst, s)
|
||||
}
|
||||
return dst
|
||||
case []interface{}:
|
||||
dst = append(dst, arg...)
|
||||
return dst
|
||||
case map[string]interface{}:
|
||||
for k, v := range arg {
|
||||
dst = append(dst, k, v)
|
||||
}
|
||||
return dst
|
||||
case map[string]string:
|
||||
for k, v := range arg {
|
||||
dst = append(dst, k, v)
|
||||
}
|
||||
return dst
|
||||
case time.Time, time.Duration, encoding.BinaryMarshaler, net.IP:
|
||||
return append(dst, arg)
|
||||
default:
|
||||
// scan struct field
|
||||
v := reflect.ValueOf(arg)
|
||||
if v.Type().Kind() == reflect.Ptr {
|
||||
if v.IsNil() {
|
||||
// error: arg is not a valid object
|
||||
return dst
|
||||
}
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if v.Type().Kind() == reflect.Struct {
|
||||
return appendStructField(dst, v)
|
||||
}
|
||||
|
||||
return append(dst, arg)
|
||||
}
|
||||
}
|
||||
|
||||
// appendStructField appends the field and value held by the structure v to dst, and returns the appended dst.
|
||||
func appendStructField(dst []interface{}, v reflect.Value) []interface{} {
|
||||
typ := v.Type()
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
tag := typ.Field(i).Tag.Get("redis")
|
||||
if tag == "" || tag == "-" {
|
||||
continue
|
||||
}
|
||||
name, opt, _ := strings.Cut(tag, ",")
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
field := v.Field(i)
|
||||
|
||||
// miss field
|
||||
if omitEmpty(opt) && isEmptyValue(field) {
|
||||
continue
|
||||
}
|
||||
|
||||
if field.CanInterface() {
|
||||
dst = append(dst, name, field.Interface())
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
func omitEmpty(opt string) bool {
|
||||
for opt != "" {
|
||||
var name string
|
||||
name, opt, _ = strings.Cut(opt, ",")
|
||||
if name == "omitempty" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEmptyValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
|
||||
return v.Len() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Interface, reflect.Pointer:
|
||||
return v.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Cmdable interface {
|
||||
Pipeline() Pipeliner
|
||||
Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error)
|
||||
|
||||
TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error)
|
||||
TxPipeline() Pipeliner
|
||||
|
||||
Command(ctx context.Context) *CommandsInfoCmd
|
||||
CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd
|
||||
CommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd
|
||||
CommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd
|
||||
ClientGetName(ctx context.Context) *StringCmd
|
||||
Echo(ctx context.Context, message interface{}) *StringCmd
|
||||
Ping(ctx context.Context) *StatusCmd
|
||||
Quit(ctx context.Context) *StatusCmd
|
||||
Unlink(ctx context.Context, keys ...string) *IntCmd
|
||||
|
||||
BgRewriteAOF(ctx context.Context) *StatusCmd
|
||||
BgSave(ctx context.Context) *StatusCmd
|
||||
ClientKill(ctx context.Context, ipPort string) *StatusCmd
|
||||
ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd
|
||||
ClientList(ctx context.Context) *StringCmd
|
||||
ClientInfo(ctx context.Context) *ClientInfoCmd
|
||||
ClientPause(ctx context.Context, dur time.Duration) *BoolCmd
|
||||
ClientUnpause(ctx context.Context) *BoolCmd
|
||||
ClientID(ctx context.Context) *IntCmd
|
||||
ClientUnblock(ctx context.Context, id int64) *IntCmd
|
||||
ClientUnblockWithError(ctx context.Context, id int64) *IntCmd
|
||||
ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd
|
||||
ConfigResetStat(ctx context.Context) *StatusCmd
|
||||
ConfigSet(ctx context.Context, parameter, value string) *StatusCmd
|
||||
ConfigRewrite(ctx context.Context) *StatusCmd
|
||||
DBSize(ctx context.Context) *IntCmd
|
||||
FlushAll(ctx context.Context) *StatusCmd
|
||||
FlushAllAsync(ctx context.Context) *StatusCmd
|
||||
FlushDB(ctx context.Context) *StatusCmd
|
||||
FlushDBAsync(ctx context.Context) *StatusCmd
|
||||
Info(ctx context.Context, section ...string) *StringCmd
|
||||
LastSave(ctx context.Context) *IntCmd
|
||||
Save(ctx context.Context) *StatusCmd
|
||||
Shutdown(ctx context.Context) *StatusCmd
|
||||
ShutdownSave(ctx context.Context) *StatusCmd
|
||||
ShutdownNoSave(ctx context.Context) *StatusCmd
|
||||
SlaveOf(ctx context.Context, host, port string) *StatusCmd
|
||||
SlowLogGet(ctx context.Context, num int64) *SlowLogCmd
|
||||
Time(ctx context.Context) *TimeCmd
|
||||
DebugObject(ctx context.Context, key string) *StringCmd
|
||||
MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd
|
||||
|
||||
ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd
|
||||
|
||||
ACLCmdable
|
||||
BitMapCmdable
|
||||
ClusterCmdable
|
||||
GearsCmdable
|
||||
GenericCmdable
|
||||
GeoCmdable
|
||||
HashCmdable
|
||||
HyperLogLogCmdable
|
||||
ListCmdable
|
||||
ProbabilisticCmdable
|
||||
PubSubCmdable
|
||||
ScriptingFunctionsCmdable
|
||||
SetCmdable
|
||||
SortedSetCmdable
|
||||
StringCmdable
|
||||
StreamCmdable
|
||||
TimeseriesCmdable
|
||||
JSONCmdable
|
||||
}
|
||||
|
||||
type StatefulCmdable interface {
|
||||
Cmdable
|
||||
Auth(ctx context.Context, password string) *StatusCmd
|
||||
AuthACL(ctx context.Context, username, password string) *StatusCmd
|
||||
Select(ctx context.Context, index int) *StatusCmd
|
||||
SwapDB(ctx context.Context, index1, index2 int) *StatusCmd
|
||||
ClientSetName(ctx context.Context, name string) *BoolCmd
|
||||
ClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd
|
||||
Hello(ctx context.Context, ver int, username, password, clientName string) *MapStringInterfaceCmd
|
||||
}
|
||||
|
||||
var (
|
||||
_ Cmdable = (*Client)(nil)
|
||||
_ Cmdable = (*Tx)(nil)
|
||||
_ Cmdable = (*Ring)(nil)
|
||||
_ Cmdable = (*ClusterClient)(nil)
|
||||
)
|
||||
|
||||
type cmdable func(ctx context.Context, cmd Cmder) error
|
||||
|
||||
type statefulCmdable func(ctx context.Context, cmd Cmder) error
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func (c statefulCmdable) Auth(ctx context.Context, password string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "auth", password)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// AuthACL Perform an AUTH command, using the given user and pass.
|
||||
// Should be used to authenticate the current connection with one of the connections defined in the ACL list
|
||||
// when connecting to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
|
||||
func (c statefulCmdable) AuthACL(ctx context.Context, username, password string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "auth", username, password)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Wait(ctx context.Context, numSlaves int, timeout time.Duration) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "wait", numSlaves, int(timeout/time.Millisecond))
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) WaitAOF(ctx context.Context, numLocal, numSlaves int, timeout time.Duration) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "waitAOF", numLocal, numSlaves, int(timeout/time.Millisecond))
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c statefulCmdable) Select(ctx context.Context, index int) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "select", index)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c statefulCmdable) SwapDB(ctx context.Context, index1, index2 int) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "swapdb", index1, index2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ClientSetName assigns a name to the connection.
|
||||
func (c statefulCmdable) ClientSetName(ctx context.Context, name string) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "client", "setname", name)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ClientSetInfo sends a CLIENT SETINFO command with the provided info.
|
||||
func (c statefulCmdable) ClientSetInfo(ctx context.Context, info LibraryInfo) *StatusCmd {
|
||||
err := info.Validate()
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
var cmd *StatusCmd
|
||||
if info.LibName != nil {
|
||||
libName := fmt.Sprintf("go-redis(%s,%s)", *info.LibName, runtime.Version())
|
||||
cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-NAME", libName)
|
||||
} else {
|
||||
cmd = NewStatusCmd(ctx, "client", "setinfo", "LIB-VER", *info.LibVer)
|
||||
}
|
||||
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Validate checks if only one field in the struct is non-nil.
|
||||
func (info LibraryInfo) Validate() error {
|
||||
if info.LibName != nil && info.LibVer != nil {
|
||||
return errors.New("both LibName and LibVer cannot be set at the same time")
|
||||
}
|
||||
if info.LibName == nil && info.LibVer == nil {
|
||||
return errors.New("at least one of LibName and LibVer should be set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Hello Set the resp protocol used.
|
||||
func (c statefulCmdable) Hello(ctx context.Context,
|
||||
ver int, username, password, clientName string,
|
||||
) *MapStringInterfaceCmd {
|
||||
args := make([]interface{}, 0, 7)
|
||||
args = append(args, "hello", ver)
|
||||
if password != "" {
|
||||
if username != "" {
|
||||
args = append(args, "auth", username, password)
|
||||
} else {
|
||||
args = append(args, "auth", "default", password)
|
||||
}
|
||||
}
|
||||
if clientName != "" {
|
||||
args = append(args, "setname", clientName)
|
||||
}
|
||||
cmd := NewMapStringInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func (c cmdable) Command(ctx context.Context) *CommandsInfoCmd {
|
||||
cmd := NewCommandsInfoCmd(ctx, "command")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// FilterBy is used for the `CommandList` command parameter.
|
||||
type FilterBy struct {
|
||||
Module string
|
||||
ACLCat string
|
||||
Pattern string
|
||||
}
|
||||
|
||||
func (c cmdable) CommandList(ctx context.Context, filter *FilterBy) *StringSliceCmd {
|
||||
args := make([]interface{}, 0, 5)
|
||||
args = append(args, "command", "list")
|
||||
if filter != nil {
|
||||
if filter.Module != "" {
|
||||
args = append(args, "filterby", "module", filter.Module)
|
||||
} else if filter.ACLCat != "" {
|
||||
args = append(args, "filterby", "aclcat", filter.ACLCat)
|
||||
} else if filter.Pattern != "" {
|
||||
args = append(args, "filterby", "pattern", filter.Pattern)
|
||||
}
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) CommandGetKeys(ctx context.Context, commands ...interface{}) *StringSliceCmd {
|
||||
args := make([]interface{}, 2+len(commands))
|
||||
args[0] = "command"
|
||||
args[1] = "getkeys"
|
||||
copy(args[2:], commands)
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) CommandGetKeysAndFlags(ctx context.Context, commands ...interface{}) *KeyFlagsCmd {
|
||||
args := make([]interface{}, 2+len(commands))
|
||||
args[0] = "command"
|
||||
args[1] = "getkeysandflags"
|
||||
copy(args[2:], commands)
|
||||
cmd := NewKeyFlagsCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ClientGetName returns the name of the connection.
|
||||
func (c cmdable) ClientGetName(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "client", "getname")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Echo(ctx context.Context, message interface{}) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "echo", message)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Ping(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "ping")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Quit(_ context.Context) *StatusCmd {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func (c cmdable) BgRewriteAOF(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "bgrewriteaof")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) BgSave(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "bgsave")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientKill(ctx context.Context, ipPort string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "client", "kill", ipPort)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ClientKillByFilter is new style syntax, while the ClientKill is old
|
||||
//
|
||||
// CLIENT KILL <option> [value] ... <option> [value]
|
||||
func (c cmdable) ClientKillByFilter(ctx context.Context, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 2+len(keys))
|
||||
args[0] = "client"
|
||||
args[1] = "kill"
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientList(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "client", "list")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientPause(ctx context.Context, dur time.Duration) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "client", "pause", formatMs(ctx, dur))
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientUnpause(ctx context.Context) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "client", "unpause")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientID(ctx context.Context) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "client", "id")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientUnblock(ctx context.Context, id int64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "client", "unblock", id)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientUnblockWithError(ctx context.Context, id int64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "client", "unblock", id, "error")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ClientInfo(ctx context.Context) *ClientInfoCmd {
|
||||
cmd := NewClientInfoCmd(ctx, "client", "info")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------
|
||||
|
||||
func (c cmdable) ConfigGet(ctx context.Context, parameter string) *MapStringStringCmd {
|
||||
cmd := NewMapStringStringCmd(ctx, "config", "get", parameter)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ConfigResetStat(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "config", "resetstat")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ConfigSet(ctx context.Context, parameter, value string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "config", "set", parameter, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ConfigRewrite(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "config", "rewrite")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) DBSize(ctx context.Context) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "dbsize")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FlushAll(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "flushall")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FlushAllAsync(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "flushall", "async")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FlushDB(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "flushdb")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FlushDBAsync(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "flushdb", "async")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Info(ctx context.Context, sections ...string) *StringCmd {
|
||||
args := make([]interface{}, 1+len(sections))
|
||||
args[0] = "info"
|
||||
for i, section := range sections {
|
||||
args[i+1] = section
|
||||
}
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) InfoMap(ctx context.Context, sections ...string) *InfoCmd {
|
||||
args := make([]interface{}, 1+len(sections))
|
||||
args[0] = "info"
|
||||
for i, section := range sections {
|
||||
args[i+1] = section
|
||||
}
|
||||
cmd := NewInfoCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LastSave(ctx context.Context) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "lastsave")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Save(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "save")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) shutdown(ctx context.Context, modifier string) *StatusCmd {
|
||||
var args []interface{}
|
||||
if modifier == "" {
|
||||
args = []interface{}{"shutdown"}
|
||||
} else {
|
||||
args = []interface{}{"shutdown", modifier}
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
if err := cmd.Err(); err != nil {
|
||||
if err == io.EOF {
|
||||
// Server quit as expected.
|
||||
cmd.err = nil
|
||||
}
|
||||
} else {
|
||||
// Server did not quit. String reply contains the reason.
|
||||
cmd.err = errors.New(cmd.val)
|
||||
cmd.val = ""
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Shutdown(ctx context.Context) *StatusCmd {
|
||||
return c.shutdown(ctx, "")
|
||||
}
|
||||
|
||||
func (c cmdable) ShutdownSave(ctx context.Context) *StatusCmd {
|
||||
return c.shutdown(ctx, "save")
|
||||
}
|
||||
|
||||
func (c cmdable) ShutdownNoSave(ctx context.Context) *StatusCmd {
|
||||
return c.shutdown(ctx, "nosave")
|
||||
}
|
||||
|
||||
func (c cmdable) SlaveOf(ctx context.Context, host, port string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "slaveof", 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)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Sync(_ context.Context) {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (c cmdable) Time(ctx context.Context) *TimeCmd {
|
||||
cmd := NewTimeCmd(ctx, "time")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) DebugObject(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "debug", "object", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) MemoryUsage(ctx context.Context, key string, samples ...int) *IntCmd {
|
||||
args := []interface{}{"memory", "usage", key}
|
||||
if len(samples) > 0 {
|
||||
if len(samples) != 1 {
|
||||
panic("MemoryUsage expects single sample count")
|
||||
}
|
||||
args = append(args, "SAMPLES", samples[0])
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// ModuleLoadexConfig struct is used to specify the arguments for the MODULE LOADEX command of redis.
|
||||
// `MODULE LOADEX path [CONFIG name value [CONFIG name value ...]] [ARGS args [args ...]]`
|
||||
type ModuleLoadexConfig struct {
|
||||
Path string
|
||||
Conf map[string]interface{}
|
||||
Args []interface{}
|
||||
}
|
||||
|
||||
func (c *ModuleLoadexConfig) toArgs() []interface{} {
|
||||
args := make([]interface{}, 3, 3+len(c.Conf)*3+len(c.Args)*2)
|
||||
args[0] = "MODULE"
|
||||
args[1] = "LOADEX"
|
||||
args[2] = c.Path
|
||||
for k, v := range c.Conf {
|
||||
args = append(args, "CONFIG", k, v)
|
||||
}
|
||||
for _, arg := range c.Args {
|
||||
args = append(args, "ARGS", arg)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// ModuleLoadex Redis `MODULE LOADEX path [CONFIG name value [CONFIG name value ...]] [ARGS args [args ...]]` command.
|
||||
func (c cmdable) ModuleLoadex(ctx context.Context, conf *ModuleLoadexConfig) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, conf.toArgs()...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
/*
|
||||
Monitor - represents a Redis MONITOR command, allowing the user to capture
|
||||
and process all commands sent to a Redis server. This mimics the behavior of
|
||||
MONITOR in the redis-cli.
|
||||
|
||||
Notes:
|
||||
- Using MONITOR blocks the connection to the server for itself. It needs a dedicated connection
|
||||
- The user should create a channel of type string
|
||||
- This runs concurrently in the background. Trigger via the Start and Stop functions
|
||||
See further: Redis MONITOR command: https://redis.io/commands/monitor
|
||||
*/
|
||||
func (c cmdable) Monitor(ctx context.Context, ch chan string) *MonitorCmd {
|
||||
cmd := newMonitorCmd(ctx, ch)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
Generated
Vendored
Generated
Vendored
+15
-3
@@ -2,17 +2,29 @@ package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/go-redis/redis/v8/internal/proto"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
// ErrClosed performs any operation on the closed client will return this error.
|
||||
var ErrClosed = pool.ErrClosed
|
||||
|
||||
// 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
|
||||
if !errors.As(err, &rErr) {
|
||||
return false
|
||||
}
|
||||
msg := rErr.Error()
|
||||
msg = strings.TrimPrefix(msg, "ERR ") // KVRocks adds such prefix
|
||||
return strings.HasPrefix(msg, prefix)
|
||||
}
|
||||
|
||||
type Error interface {
|
||||
error
|
||||
|
||||
@@ -91,7 +103,7 @@ func isBadConn(err error, allowTimeout bool, addr string) bool {
|
||||
|
||||
if allowTimeout {
|
||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||
return !netErr.Temporary()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type GearsCmdable interface {
|
||||
TFunctionLoad(ctx context.Context, lib string) *StatusCmd
|
||||
TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd
|
||||
TFunctionDelete(ctx context.Context, libName string) *StatusCmd
|
||||
TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd
|
||||
TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd
|
||||
TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
|
||||
TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
|
||||
TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd
|
||||
TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd
|
||||
}
|
||||
|
||||
type TFunctionLoadOptions struct {
|
||||
Replace bool
|
||||
Config string
|
||||
}
|
||||
|
||||
type TFunctionListOptions struct {
|
||||
Withcode bool
|
||||
Verbose int
|
||||
Library string
|
||||
}
|
||||
|
||||
type TFCallOptions struct {
|
||||
Keys []string
|
||||
Arguments []string
|
||||
}
|
||||
|
||||
// TFunctionLoad - load a new JavaScript library into Redis.
|
||||
// For more information - https://redis.io/commands/tfunction-load/
|
||||
func (c cmdable) TFunctionLoad(ctx context.Context, lib string) *StatusCmd {
|
||||
args := []interface{}{"TFUNCTION", "LOAD", lib}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFunctionLoadArgs(ctx context.Context, lib string, options *TFunctionLoadOptions) *StatusCmd {
|
||||
args := []interface{}{"TFUNCTION", "LOAD"}
|
||||
if options != nil {
|
||||
if options.Replace {
|
||||
args = append(args, "REPLACE")
|
||||
}
|
||||
if options.Config != "" {
|
||||
args = append(args, "CONFIG", options.Config)
|
||||
}
|
||||
}
|
||||
args = append(args, lib)
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFunctionDelete - delete a JavaScript library from Redis.
|
||||
// For more information - https://redis.io/commands/tfunction-delete/
|
||||
func (c cmdable) TFunctionDelete(ctx context.Context, libName string) *StatusCmd {
|
||||
args := []interface{}{"TFUNCTION", "DELETE", libName}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFunctionList - list the functions with additional information about each function.
|
||||
// For more information - https://redis.io/commands/tfunction-list/
|
||||
func (c cmdable) TFunctionList(ctx context.Context) *MapStringInterfaceSliceCmd {
|
||||
args := []interface{}{"TFUNCTION", "LIST"}
|
||||
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFunctionListArgs(ctx context.Context, options *TFunctionListOptions) *MapStringInterfaceSliceCmd {
|
||||
args := []interface{}{"TFUNCTION", "LIST"}
|
||||
if options != nil {
|
||||
if options.Withcode {
|
||||
args = append(args, "WITHCODE")
|
||||
}
|
||||
if options.Verbose != 0 {
|
||||
v := strings.Repeat("v", options.Verbose)
|
||||
args = append(args, v)
|
||||
}
|
||||
if options.Library != "" {
|
||||
args = append(args, "LIBRARY", options.Library)
|
||||
}
|
||||
}
|
||||
cmd := NewMapStringInterfaceSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFCall - invoke a function.
|
||||
// For more information - https://redis.io/commands/tfcall/
|
||||
func (c cmdable) TFCall(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
|
||||
lf := libName + "." + funcName
|
||||
args := []interface{}{"TFCALL", lf, numKeys}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFCallArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
|
||||
lf := libName + "." + funcName
|
||||
args := []interface{}{"TFCALL", lf, numKeys}
|
||||
if options != nil {
|
||||
for _, key := range options.Keys {
|
||||
args = append(args, key)
|
||||
}
|
||||
for _, key := range options.Arguments {
|
||||
args = append(args, key)
|
||||
}
|
||||
}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TFCallASYNC - invoke an asynchronous JavaScript function (coroutine).
|
||||
// For more information - https://redis.io/commands/TFCallASYNC/
|
||||
func (c cmdable) TFCallASYNC(ctx context.Context, libName string, funcName string, numKeys int) *Cmd {
|
||||
lf := fmt.Sprintf("%s.%s", libName, funcName)
|
||||
args := []interface{}{"TFCALLASYNC", lf, numKeys}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TFCallASYNCArgs(ctx context.Context, libName string, funcName string, numKeys int, options *TFCallOptions) *Cmd {
|
||||
lf := fmt.Sprintf("%s.%s", libName, funcName)
|
||||
args := []interface{}{"TFCALLASYNC", lf, numKeys}
|
||||
if options != nil {
|
||||
for _, key := range options.Keys {
|
||||
args = append(args, key)
|
||||
}
|
||||
for _, key := range options.Arguments {
|
||||
args = append(args, key)
|
||||
}
|
||||
}
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+377
@@ -0,0 +1,377 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GenericCmdable interface {
|
||||
Del(ctx context.Context, keys ...string) *IntCmd
|
||||
Dump(ctx context.Context, key string) *StringCmd
|
||||
Exists(ctx context.Context, keys ...string) *IntCmd
|
||||
Expire(ctx context.Context, key string, expiration time.Duration) *BoolCmd
|
||||
ExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd
|
||||
ExpireTime(ctx context.Context, key string) *DurationCmd
|
||||
ExpireNX(ctx context.Context, key string, expiration time.Duration) *BoolCmd
|
||||
ExpireXX(ctx context.Context, key string, expiration time.Duration) *BoolCmd
|
||||
ExpireGT(ctx context.Context, key string, expiration time.Duration) *BoolCmd
|
||||
ExpireLT(ctx context.Context, key string, expiration time.Duration) *BoolCmd
|
||||
Keys(ctx context.Context, pattern string) *StringSliceCmd
|
||||
Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd
|
||||
Move(ctx context.Context, key string, db int) *BoolCmd
|
||||
ObjectRefCount(ctx context.Context, key string) *IntCmd
|
||||
ObjectEncoding(ctx context.Context, key string) *StringCmd
|
||||
ObjectIdleTime(ctx context.Context, key string) *DurationCmd
|
||||
Persist(ctx context.Context, key string) *BoolCmd
|
||||
PExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd
|
||||
PExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd
|
||||
PExpireTime(ctx context.Context, key string) *DurationCmd
|
||||
PTTL(ctx context.Context, key string) *DurationCmd
|
||||
RandomKey(ctx context.Context) *StringCmd
|
||||
Rename(ctx context.Context, key, newkey string) *StatusCmd
|
||||
RenameNX(ctx context.Context, key, newkey string) *BoolCmd
|
||||
Restore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd
|
||||
RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd
|
||||
Sort(ctx context.Context, key string, sort *Sort) *StringSliceCmd
|
||||
SortRO(ctx context.Context, key string, sort *Sort) *StringSliceCmd
|
||||
SortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd
|
||||
SortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd
|
||||
Touch(ctx context.Context, keys ...string) *IntCmd
|
||||
TTL(ctx context.Context, key string) *DurationCmd
|
||||
Type(ctx context.Context, key string) *StatusCmd
|
||||
Copy(ctx context.Context, sourceKey string, destKey string, db int, replace bool) *IntCmd
|
||||
|
||||
Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd
|
||||
ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *ScanCmd
|
||||
}
|
||||
|
||||
func (c cmdable) Del(ctx context.Context, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "del"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Unlink(ctx context.Context, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "unlink"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Dump(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "dump", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Exists(ctx context.Context, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "exists"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Expire(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
|
||||
return c.expire(ctx, key, expiration, "")
|
||||
}
|
||||
|
||||
func (c cmdable) ExpireNX(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
|
||||
return c.expire(ctx, key, expiration, "NX")
|
||||
}
|
||||
|
||||
func (c cmdable) ExpireXX(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
|
||||
return c.expire(ctx, key, expiration, "XX")
|
||||
}
|
||||
|
||||
func (c cmdable) ExpireGT(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
|
||||
return c.expire(ctx, key, expiration, "GT")
|
||||
}
|
||||
|
||||
func (c cmdable) ExpireLT(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
|
||||
return c.expire(ctx, key, expiration, "LT")
|
||||
}
|
||||
|
||||
func (c cmdable) expire(
|
||||
ctx context.Context, key string, expiration time.Duration, mode string,
|
||||
) *BoolCmd {
|
||||
args := make([]interface{}, 3, 4)
|
||||
args[0] = "expire"
|
||||
args[1] = key
|
||||
args[2] = formatSec(ctx, expiration)
|
||||
if mode != "" {
|
||||
args = append(args, mode)
|
||||
}
|
||||
|
||||
cmd := NewBoolCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "expireat", key, tm.Unix())
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ExpireTime(ctx context.Context, key string) *DurationCmd {
|
||||
cmd := NewDurationCmd(ctx, time.Second, "expiretime", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Keys(ctx context.Context, pattern string) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "keys", pattern)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Migrate(ctx context.Context, host, port, key string, db int, timeout time.Duration) *StatusCmd {
|
||||
cmd := NewStatusCmd(
|
||||
ctx,
|
||||
"migrate",
|
||||
host,
|
||||
port,
|
||||
key,
|
||||
db,
|
||||
formatMs(ctx, timeout),
|
||||
)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Move(ctx context.Context, key string, db int) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "move", key, db)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ObjectRefCount(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "object", "refcount", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ObjectEncoding(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "object", "encoding", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ObjectIdleTime(ctx context.Context, key string) *DurationCmd {
|
||||
cmd := NewDurationCmd(ctx, time.Second, "object", "idletime", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Persist(ctx context.Context, key string) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "persist", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PExpire(ctx context.Context, key string, expiration time.Duration) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "pexpire", key, formatMs(ctx, expiration))
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PExpireAt(ctx context.Context, key string, tm time.Time) *BoolCmd {
|
||||
cmd := NewBoolCmd(
|
||||
ctx,
|
||||
"pexpireat",
|
||||
key,
|
||||
tm.UnixNano()/int64(time.Millisecond),
|
||||
)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PExpireTime(ctx context.Context, key string) *DurationCmd {
|
||||
cmd := NewDurationCmd(ctx, time.Millisecond, "pexpiretime", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PTTL(ctx context.Context, key string) *DurationCmd {
|
||||
cmd := NewDurationCmd(ctx, time.Millisecond, "pttl", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RandomKey(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "randomkey")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Rename(ctx context.Context, key, newkey string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "rename", key, newkey)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RenameNX(ctx context.Context, key, newkey string) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "renamenx", key, newkey)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Restore(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd {
|
||||
cmd := NewStatusCmd(
|
||||
ctx,
|
||||
"restore",
|
||||
key,
|
||||
formatMs(ctx, ttl),
|
||||
value,
|
||||
)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RestoreReplace(ctx context.Context, key string, ttl time.Duration, value string) *StatusCmd {
|
||||
cmd := NewStatusCmd(
|
||||
ctx,
|
||||
"restore",
|
||||
key,
|
||||
formatMs(ctx, ttl),
|
||||
value,
|
||||
"replace",
|
||||
)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type Sort struct {
|
||||
By string
|
||||
Offset, Count int64
|
||||
Get []string
|
||||
Order string
|
||||
Alpha bool
|
||||
}
|
||||
|
||||
func (sort *Sort) args(command, key string) []interface{} {
|
||||
args := []interface{}{command, key}
|
||||
|
||||
if sort.By != "" {
|
||||
args = append(args, "by", sort.By)
|
||||
}
|
||||
if sort.Offset != 0 || sort.Count != 0 {
|
||||
args = append(args, "limit", sort.Offset, sort.Count)
|
||||
}
|
||||
for _, get := range sort.Get {
|
||||
args = append(args, "get", get)
|
||||
}
|
||||
if sort.Order != "" {
|
||||
args = append(args, sort.Order)
|
||||
}
|
||||
if sort.Alpha {
|
||||
args = append(args, "alpha")
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (c cmdable) SortRO(ctx context.Context, key string, sort *Sort) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, sort.args("sort_ro", key)...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Sort(ctx context.Context, key string, sort *Sort) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, sort.args("sort", key)...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SortStore(ctx context.Context, key, store string, sort *Sort) *IntCmd {
|
||||
args := sort.args("sort", key)
|
||||
if store != "" {
|
||||
args = append(args, "store", store)
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SortInterfaces(ctx context.Context, key string, sort *Sort) *SliceCmd {
|
||||
cmd := NewSliceCmd(ctx, sort.args("sort", key)...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Touch(ctx context.Context, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, len(keys)+1)
|
||||
args[0] = "touch"
|
||||
for i, key := range keys {
|
||||
args[i+1] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) TTL(ctx context.Context, key string) *DurationCmd {
|
||||
cmd := NewDurationCmd(ctx, time.Second, "ttl", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Type(ctx context.Context, key string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "type", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Copy(ctx context.Context, sourceKey, destKey string, db int, replace bool) *IntCmd {
|
||||
args := []interface{}{"copy", sourceKey, destKey, "DB", db}
|
||||
if replace {
|
||||
args = append(args, "REPLACE")
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func (c cmdable) Scan(ctx context.Context, cursor uint64, match string, count int64) *ScanCmd {
|
||||
args := []interface{}{"scan", cursor}
|
||||
if match != "" {
|
||||
args = append(args, "match", match)
|
||||
}
|
||||
if count > 0 {
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ScanType(ctx context.Context, cursor uint64, match string, count int64, keyType string) *ScanCmd {
|
||||
args := []interface{}{"scan", cursor}
|
||||
if match != "" {
|
||||
args = append(args, "match", match)
|
||||
}
|
||||
if count > 0 {
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
if keyType != "" {
|
||||
args = append(args, "type", keyType)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type GeoCmdable interface {
|
||||
GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd
|
||||
GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd
|
||||
GeoRadius(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *GeoLocationCmd
|
||||
GeoRadiusStore(ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery) *IntCmd
|
||||
GeoRadiusByMember(ctx context.Context, key, member string, query *GeoRadiusQuery) *GeoLocationCmd
|
||||
GeoRadiusByMemberStore(ctx context.Context, key, member string, query *GeoRadiusQuery) *IntCmd
|
||||
GeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd
|
||||
GeoSearchLocation(ctx context.Context, key string, q *GeoSearchLocationQuery) *GeoSearchLocationCmd
|
||||
GeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd
|
||||
GeoDist(ctx context.Context, key string, member1, member2, unit string) *FloatCmd
|
||||
GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd
|
||||
}
|
||||
|
||||
func (c cmdable) GeoAdd(ctx context.Context, key string, geoLocation ...*GeoLocation) *IntCmd {
|
||||
args := make([]interface{}, 2+3*len(geoLocation))
|
||||
args[0] = "geoadd"
|
||||
args[1] = key
|
||||
for i, eachLoc := range geoLocation {
|
||||
args[2+3*i] = eachLoc.Longitude
|
||||
args[2+3*i+1] = eachLoc.Latitude
|
||||
args[2+3*i+2] = eachLoc.Name
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GeoRadius is a read-only GEORADIUS_RO command.
|
||||
func (c cmdable) GeoRadius(
|
||||
ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery,
|
||||
) *GeoLocationCmd {
|
||||
cmd := NewGeoLocationCmd(ctx, query, "georadius_ro", key, longitude, latitude)
|
||||
if query.Store != "" || query.StoreDist != "" {
|
||||
cmd.SetErr(errors.New("GeoRadius does not support Store or StoreDist"))
|
||||
return cmd
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GeoRadiusStore is a writing GEORADIUS command.
|
||||
func (c cmdable) GeoRadiusStore(
|
||||
ctx context.Context, key string, longitude, latitude float64, query *GeoRadiusQuery,
|
||||
) *IntCmd {
|
||||
args := geoLocationArgs(query, "georadius", key, longitude, latitude)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
if query.Store == "" && query.StoreDist == "" {
|
||||
cmd.SetErr(errors.New("GeoRadiusStore requires Store or StoreDist"))
|
||||
return cmd
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GeoRadiusByMember is a read-only GEORADIUSBYMEMBER_RO command.
|
||||
func (c cmdable) GeoRadiusByMember(
|
||||
ctx context.Context, key, member string, query *GeoRadiusQuery,
|
||||
) *GeoLocationCmd {
|
||||
cmd := NewGeoLocationCmd(ctx, query, "georadiusbymember_ro", key, member)
|
||||
if query.Store != "" || query.StoreDist != "" {
|
||||
cmd.SetErr(errors.New("GeoRadiusByMember does not support Store or StoreDist"))
|
||||
return cmd
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GeoRadiusByMemberStore is a writing GEORADIUSBYMEMBER command.
|
||||
func (c cmdable) GeoRadiusByMemberStore(
|
||||
ctx context.Context, key, member string, query *GeoRadiusQuery,
|
||||
) *IntCmd {
|
||||
args := geoLocationArgs(query, "georadiusbymember", key, member)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
if query.Store == "" && query.StoreDist == "" {
|
||||
cmd.SetErr(errors.New("GeoRadiusByMemberStore requires Store or StoreDist"))
|
||||
return cmd
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GeoSearch(ctx context.Context, key string, q *GeoSearchQuery) *StringSliceCmd {
|
||||
args := make([]interface{}, 0, 13)
|
||||
args = append(args, "geosearch", key)
|
||||
args = geoSearchArgs(q, args)
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GeoSearchLocation(
|
||||
ctx context.Context, key string, q *GeoSearchLocationQuery,
|
||||
) *GeoSearchLocationCmd {
|
||||
args := make([]interface{}, 0, 16)
|
||||
args = append(args, "geosearch", key)
|
||||
args = geoSearchLocationArgs(q, args)
|
||||
cmd := NewGeoSearchLocationCmd(ctx, q, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GeoSearchStore(ctx context.Context, key, store string, q *GeoSearchStoreQuery) *IntCmd {
|
||||
args := make([]interface{}, 0, 15)
|
||||
args = append(args, "geosearchstore", store, key)
|
||||
args = geoSearchArgs(&q.GeoSearchQuery, args)
|
||||
if q.StoreDist {
|
||||
args = append(args, "storedist")
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GeoDist(
|
||||
ctx context.Context, key string, member1, member2, unit string,
|
||||
) *FloatCmd {
|
||||
if unit == "" {
|
||||
unit = "km"
|
||||
}
|
||||
cmd := NewFloatCmd(ctx, "geodist", key, member1, member2, unit)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GeoHash(ctx context.Context, key string, members ...string) *StringSliceCmd {
|
||||
args := make([]interface{}, 2+len(members))
|
||||
args[0] = "geohash"
|
||||
args[1] = key
|
||||
for i, member := range members {
|
||||
args[2+i] = member
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GeoPos(ctx context.Context, key string, members ...string) *GeoPosCmd {
|
||||
args := make([]interface{}, 2+len(members))
|
||||
args[0] = "geopos"
|
||||
args[1] = key
|
||||
for i, member := range members {
|
||||
args[2+i] = member
|
||||
}
|
||||
cmd := NewGeoPosCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
type HashCmdable interface {
|
||||
HDel(ctx context.Context, key string, fields ...string) *IntCmd
|
||||
HExists(ctx context.Context, key, field string) *BoolCmd
|
||||
HGet(ctx context.Context, key, field string) *StringCmd
|
||||
HGetAll(ctx context.Context, key string) *MapStringStringCmd
|
||||
HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd
|
||||
HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd
|
||||
HKeys(ctx context.Context, key string) *StringSliceCmd
|
||||
HLen(ctx context.Context, key string) *IntCmd
|
||||
HMGet(ctx context.Context, key string, fields ...string) *SliceCmd
|
||||
HSet(ctx context.Context, key string, values ...interface{}) *IntCmd
|
||||
HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd
|
||||
HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd
|
||||
HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
|
||||
HVals(ctx context.Context, key string) *StringSliceCmd
|
||||
HRandField(ctx context.Context, key string, count int) *StringSliceCmd
|
||||
HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd
|
||||
}
|
||||
|
||||
func (c cmdable) HDel(ctx context.Context, key string, fields ...string) *IntCmd {
|
||||
args := make([]interface{}, 2+len(fields))
|
||||
args[0] = "hdel"
|
||||
args[1] = key
|
||||
for i, field := range fields {
|
||||
args[2+i] = field
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HExists(ctx context.Context, key, field string) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "hexists", key, field)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HGet(ctx context.Context, key, field string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "hget", key, field)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HGetAll(ctx context.Context, key string) *MapStringStringCmd {
|
||||
cmd := NewMapStringStringCmd(ctx, "hgetall", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HIncrBy(ctx context.Context, key, field string, incr int64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "hincrby", key, field, incr)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HIncrByFloat(ctx context.Context, key, field string, incr float64) *FloatCmd {
|
||||
cmd := NewFloatCmd(ctx, "hincrbyfloat", key, field, incr)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HKeys(ctx context.Context, key string) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "hkeys", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HLen(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "hlen", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// HMGet returns the values for the specified fields in the hash stored at key.
|
||||
// It returns an interface{} to distinguish between empty string and nil value.
|
||||
func (c cmdable) HMGet(ctx context.Context, key string, fields ...string) *SliceCmd {
|
||||
args := make([]interface{}, 2+len(fields))
|
||||
args[0] = "hmget"
|
||||
args[1] = key
|
||||
for i, field := range fields {
|
||||
args[2+i] = field
|
||||
}
|
||||
cmd := NewSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// HSet accepts values in following formats:
|
||||
//
|
||||
// - HSet("myhash", "key1", "value1", "key2", "value2")
|
||||
//
|
||||
// - HSet("myhash", []string{"key1", "value1", "key2", "value2"})
|
||||
//
|
||||
// - HSet("myhash", map[string]interface{}{"key1": "value1", "key2": "value2"})
|
||||
//
|
||||
// Playing struct With "redis" tag.
|
||||
// type MyHash struct { Key1 string `redis:"key1"`; Key2 int `redis:"key2"` }
|
||||
//
|
||||
// - HSet("myhash", MyHash{"value1", "value2"}) Warn: redis-server >= 4.0
|
||||
//
|
||||
// For struct, can be a structure pointer type, we only parse the field whose tag is redis.
|
||||
// if you don't want the field to be read, you can use the `redis:"-"` flag to ignore it,
|
||||
// or you don't need to set the redis tag.
|
||||
// For the type of structure field, we only support simple data types:
|
||||
// string, int/uint(8,16,32,64), float(32,64), time.Time(to RFC3339Nano), time.Duration(to Nanoseconds ),
|
||||
// if you are other more complex or custom data types, please implement the encoding.BinaryMarshaler interface.
|
||||
//
|
||||
// Note that in older versions of Redis server(redis-server < 4.0), HSet only supports a single key-value pair.
|
||||
// redis-docs: https://redis.io/commands/hset (Starting with Redis version 4.0.0: Accepts multiple field and value arguments.)
|
||||
// If you are using a Struct type and the number of fields is greater than one,
|
||||
// you will receive an error similar to "ERR wrong number of arguments", you can use HMSet as a substitute.
|
||||
func (c cmdable) HSet(ctx context.Context, key string, values ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "hset"
|
||||
args[1] = key
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// HMSet is a deprecated version of HSet left for compatibility with Redis 3.
|
||||
func (c cmdable) HMSet(ctx context.Context, key string, values ...interface{}) *BoolCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "hmset"
|
||||
args[1] = key
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewBoolCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HSetNX(ctx context.Context, key, field string, value interface{}) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "hsetnx", key, field, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HVals(ctx context.Context, key string) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "hvals", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// HRandField redis-server version >= 6.2.0.
|
||||
func (c cmdable) HRandField(ctx context.Context, key string, count int) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "hrandfield", key, count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// HRandFieldWithValues redis-server version >= 6.2.0.
|
||||
func (c cmdable) HRandFieldWithValues(ctx context.Context, key string, count int) *KeyValueSliceCmd {
|
||||
cmd := NewKeyValueSliceCmd(ctx, "hrandfield", key, count, "withvalues")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) HScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
|
||||
args := []interface{}{"hscan", key, cursor}
|
||||
if match != "" {
|
||||
args = append(args, "match", match)
|
||||
}
|
||||
if count > 0 {
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
type HyperLogLogCmdable interface {
|
||||
PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd
|
||||
PFCount(ctx context.Context, keys ...string) *IntCmd
|
||||
PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd
|
||||
}
|
||||
|
||||
func (c cmdable) PFAdd(ctx context.Context, key string, els ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(els))
|
||||
args[0] = "pfadd"
|
||||
args[1] = key
|
||||
args = appendArgs(args, els)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PFCount(ctx context.Context, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "pfcount"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PFMerge(ctx context.Context, dest string, keys ...string) *StatusCmd {
|
||||
args := make([]interface{}, 2+len(keys))
|
||||
args[0] = "pfmerge"
|
||||
args[1] = dest
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
Generated
Vendored
+3
-1
@@ -4,6 +4,8 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
func AppendArg(b []byte, v interface{}) []byte {
|
||||
@@ -11,7 +13,7 @@ func AppendArg(b []byte, v interface{}) []byte {
|
||||
case nil:
|
||||
return append(b, "<nil>"...)
|
||||
case string:
|
||||
return appendUTF8String(b, Bytes(v))
|
||||
return appendUTF8String(b, util.StringToBytes(v))
|
||||
case []byte:
|
||||
return appendUTF8String(b, v)
|
||||
case int:
|
||||
Generated
Vendored
+1
-1
@@ -3,7 +3,7 @@ package hashtag
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/rand"
|
||||
"github.com/redis/go-redis/v9/internal/rand"
|
||||
)
|
||||
|
||||
const slotNumber = 16384
|
||||
Generated
Vendored
+6
@@ -10,6 +10,12 @@ import (
|
||||
// decoderFunc represents decoding functions for default built-in types.
|
||||
type decoderFunc func(reflect.Value, string) error
|
||||
|
||||
// Scanner is the interface implemented by themselves,
|
||||
// which will override the decoding behavior of decoderFunc.
|
||||
type Scanner interface {
|
||||
ScanRedis(s string) error
|
||||
}
|
||||
|
||||
var (
|
||||
// List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1).
|
||||
decoders = []decoderFunc{
|
||||
Generated
Vendored
+34
-2
@@ -1,10 +1,13 @@
|
||||
package hscan
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
// structMap contains the map of struct fields for target structs
|
||||
@@ -58,7 +61,11 @@ func newStructSpec(t reflect.Type, fieldTag string) *structSpec {
|
||||
}
|
||||
|
||||
// Use the built-in decoder.
|
||||
out.set(tag, &structField{index: i, fn: decoders[f.Type.Kind()]})
|
||||
kind := f.Type.Kind()
|
||||
if kind == reflect.Pointer {
|
||||
kind = f.Type.Elem().Kind()
|
||||
}
|
||||
out.set(tag, &structField{index: i, fn: decoders[kind]})
|
||||
}
|
||||
|
||||
return out
|
||||
@@ -84,7 +91,32 @@ func (s StructValue) Scan(key string, value string) error {
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if err := field.fn(s.value.Field(field.index), value); err != nil {
|
||||
|
||||
v := s.value.Field(field.index)
|
||||
isPtr := v.Kind() == reflect.Ptr
|
||||
|
||||
if isPtr && v.IsNil() {
|
||||
v.Set(reflect.New(v.Type().Elem()))
|
||||
}
|
||||
if !isPtr && v.Type().Name() != "" && v.CanAddr() {
|
||||
v = v.Addr()
|
||||
isPtr = true
|
||||
}
|
||||
|
||||
if isPtr && v.Type().NumMethod() > 0 && v.CanInterface() {
|
||||
switch scan := v.Interface().(type) {
|
||||
case Scanner:
|
||||
return scan.ScanRedis(value)
|
||||
case encoding.TextUnmarshaler:
|
||||
return scan.UnmarshalText(util.StringToBytes(value))
|
||||
}
|
||||
}
|
||||
|
||||
if isPtr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
if err := field.fn(v, value); err != nil {
|
||||
t := s.value.Type()
|
||||
return fmt.Errorf("cannot scan redis.result %s into struct field %s.%s of type %s, error-%s",
|
||||
value, t.Name(), t.Field(field.index).Name, t.Field(field.index).Type, err.Error())
|
||||
Generated
Vendored
+1
-1
@@ -3,7 +3,7 @@ package internal
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/rand"
|
||||
"github.com/redis/go-redis/v9/internal/rand"
|
||||
)
|
||||
|
||||
func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration {
|
||||
Generated
Vendored
Generated
Vendored
+5
-2
@@ -32,7 +32,9 @@ type Once struct {
|
||||
|
||||
// Do calls the function f if and only if Do has not been invoked
|
||||
// without error for this instance of Once. In other words, given
|
||||
// var once Once
|
||||
//
|
||||
// var once Once
|
||||
//
|
||||
// if once.Do(f) is called multiple times, only the first call will
|
||||
// invoke f, even if f has a different value in each invocation unless
|
||||
// f returns an error. A new instance of Once is required for each
|
||||
@@ -41,7 +43,8 @@ type Once struct {
|
||||
// Do is intended for initialization that must be run exactly once. Since f
|
||||
// is niladic, it may be necessary to use a function literal to capture the
|
||||
// arguments to a function to be invoked by Do:
|
||||
// err := config.once.Do(func() error { return config.init(filename) })
|
||||
//
|
||||
// err := config.once.Do(func() error { return config.init(filename) })
|
||||
func (o *Once) Do(f func() error) error {
|
||||
if atomic.LoadUint32(&o.done) == 1 {
|
||||
return nil
|
||||
Generated
Vendored
+12
-6
@@ -7,7 +7,7 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/proto"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
var noDeadline = time.Time{}
|
||||
@@ -63,9 +63,13 @@ func (cn *Conn) RemoteAddr() net.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cn *Conn) WithReader(ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error) error {
|
||||
if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||
return err
|
||||
func (cn *Conn) WithReader(
|
||||
ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error,
|
||||
) error {
|
||||
if timeout >= 0 {
|
||||
if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return fn(cn.rd)
|
||||
}
|
||||
@@ -73,8 +77,10 @@ func (cn *Conn) WithReader(ctx context.Context, timeout time.Duration, fn func(r
|
||||
func (cn *Conn) WithWriter(
|
||||
ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error,
|
||||
) error {
|
||||
if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||
return err
|
||||
if timeout >= 0 {
|
||||
if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cn.bw.Buffered() > 0 {
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos
|
||||
|
||||
package pool
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var errUnexpectedRead = errors.New("unexpected read from socket")
|
||||
|
||||
func connCheck(conn net.Conn) error {
|
||||
// Reset previous timeout.
|
||||
_ = conn.SetDeadline(time.Time{})
|
||||
|
||||
sysConn, ok := conn.(syscall.Conn)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
rawConn, err := sysConn.SyscallConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var sysErr error
|
||||
|
||||
if err := rawConn.Read(func(fd uintptr) bool {
|
||||
var buf [1]byte
|
||||
n, err := syscall.Read(int(fd), buf[:])
|
||||
switch {
|
||||
case n == 0 && err == nil:
|
||||
sysErr = io.EOF
|
||||
case n > 0:
|
||||
sysErr = errUnexpectedRead
|
||||
case err == syscall.EAGAIN || err == syscall.EWOULDBLOCK:
|
||||
sysErr = nil
|
||||
default:
|
||||
sysErr = err
|
||||
}
|
||||
return true
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sysErr
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos
|
||||
|
||||
package pool
|
||||
|
||||
import "net"
|
||||
|
||||
func connCheck(conn net.Conn) error {
|
||||
return nil
|
||||
}
|
||||
Generated
Vendored
+88
-135
@@ -8,13 +8,17 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal"
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrClosed performs any operation on the closed client will return this error.
|
||||
ErrClosed = errors.New("redis: client is closed")
|
||||
|
||||
// ErrPoolExhausted is returned from a pool connection method
|
||||
// when the maximum number of database connections in the pool has been reached.
|
||||
ErrPoolExhausted = errors.New("redis: connection pool exhausted")
|
||||
|
||||
// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
|
||||
ErrPoolTimeout = errors.New("redis: connection pool timeout")
|
||||
)
|
||||
@@ -54,16 +58,16 @@ type Pooler interface {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Dialer func(context.Context) (net.Conn, error)
|
||||
OnClose func(*Conn) error
|
||||
Dialer func(context.Context) (net.Conn, error)
|
||||
|
||||
PoolFIFO bool
|
||||
PoolSize int
|
||||
MinIdleConns int
|
||||
MaxConnAge time.Duration
|
||||
PoolTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
IdleCheckFrequency time.Duration
|
||||
PoolFIFO bool
|
||||
PoolSize int
|
||||
PoolTimeout time.Duration
|
||||
MinIdleConns int
|
||||
MaxIdleConns int
|
||||
MaxActiveConns int
|
||||
ConnMaxIdleTime time.Duration
|
||||
ConnMaxLifetime time.Duration
|
||||
}
|
||||
|
||||
type lastDialErrorWrap struct {
|
||||
@@ -71,66 +75,67 @@ type lastDialErrorWrap struct {
|
||||
}
|
||||
|
||||
type ConnPool struct {
|
||||
opt *Options
|
||||
cfg *Options
|
||||
|
||||
dialErrorsNum uint32 // atomic
|
||||
|
||||
lastDialError atomic.Value
|
||||
|
||||
queue chan struct{}
|
||||
|
||||
connsMu sync.Mutex
|
||||
conns []*Conn
|
||||
idleConns []*Conn
|
||||
connsMu sync.Mutex
|
||||
conns []*Conn
|
||||
idleConns []*Conn
|
||||
|
||||
poolSize int
|
||||
idleConnsLen int
|
||||
|
||||
stats Stats
|
||||
|
||||
_closed uint32 // atomic
|
||||
closedCh chan struct{}
|
||||
_closed uint32 // atomic
|
||||
}
|
||||
|
||||
var _ Pooler = (*ConnPool)(nil)
|
||||
|
||||
func NewConnPool(opt *Options) *ConnPool {
|
||||
p := &ConnPool{
|
||||
opt: opt,
|
||||
cfg: opt,
|
||||
|
||||
queue: make(chan struct{}, opt.PoolSize),
|
||||
conns: make([]*Conn, 0, opt.PoolSize),
|
||||
idleConns: make([]*Conn, 0, opt.PoolSize),
|
||||
closedCh: make(chan struct{}),
|
||||
}
|
||||
|
||||
p.connsMu.Lock()
|
||||
p.checkMinIdleConns()
|
||||
p.connsMu.Unlock()
|
||||
|
||||
if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
|
||||
go p.reaper(opt.IdleCheckFrequency)
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *ConnPool) checkMinIdleConns() {
|
||||
if p.opt.MinIdleConns == 0 {
|
||||
if p.cfg.MinIdleConns == 0 {
|
||||
return
|
||||
}
|
||||
for p.poolSize < p.opt.PoolSize && p.idleConnsLen < p.opt.MinIdleConns {
|
||||
p.poolSize++
|
||||
p.idleConnsLen++
|
||||
for p.poolSize < p.cfg.PoolSize && p.idleConnsLen < p.cfg.MinIdleConns {
|
||||
select {
|
||||
case p.queue <- struct{}{}:
|
||||
p.poolSize++
|
||||
p.idleConnsLen++
|
||||
|
||||
go func() {
|
||||
err := p.addIdleConn()
|
||||
if err != nil && err != ErrClosed {
|
||||
p.connsMu.Lock()
|
||||
p.poolSize--
|
||||
p.idleConnsLen--
|
||||
p.connsMu.Unlock()
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := p.addIdleConn()
|
||||
if err != nil && err != ErrClosed {
|
||||
p.connsMu.Lock()
|
||||
p.poolSize--
|
||||
p.idleConnsLen--
|
||||
p.connsMu.Unlock()
|
||||
}
|
||||
|
||||
p.freeTurn()
|
||||
}()
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +164,14 @@ func (p *ConnPool) NewConn(ctx context.Context) (*Conn, error) {
|
||||
}
|
||||
|
||||
func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
|
||||
if p.closed() {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
if p.cfg.MaxActiveConns > 0 && p.poolSize >= p.cfg.MaxActiveConns {
|
||||
return nil, ErrPoolExhausted
|
||||
}
|
||||
|
||||
cn, err := p.dialConn(ctx, pooled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -167,16 +180,10 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
|
||||
p.connsMu.Lock()
|
||||
defer p.connsMu.Unlock()
|
||||
|
||||
// It is not allowed to add new connections to the closed connection pool.
|
||||
if p.closed() {
|
||||
_ = cn.Close()
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
p.conns = append(p.conns, cn)
|
||||
if pooled {
|
||||
// If pool is full remove the cn on next Put.
|
||||
if p.poolSize >= p.opt.PoolSize {
|
||||
if p.poolSize >= p.cfg.PoolSize {
|
||||
cn.pooled = false
|
||||
} else {
|
||||
p.poolSize++
|
||||
@@ -191,14 +198,14 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
if atomic.LoadUint32(&p.dialErrorsNum) >= uint32(p.opt.PoolSize) {
|
||||
if atomic.LoadUint32(&p.dialErrorsNum) >= uint32(p.cfg.PoolSize) {
|
||||
return nil, p.getLastDialError()
|
||||
}
|
||||
|
||||
netConn, err := p.opt.Dialer(ctx)
|
||||
netConn, err := p.cfg.Dialer(ctx)
|
||||
if err != nil {
|
||||
p.setLastDialError(err)
|
||||
if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.opt.PoolSize) {
|
||||
if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.cfg.PoolSize) {
|
||||
go p.tryDial()
|
||||
}
|
||||
return nil, err
|
||||
@@ -215,7 +222,7 @@ func (p *ConnPool) tryDial() {
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := p.opt.Dialer(context.Background())
|
||||
conn, err := p.cfg.Dialer(context.Background())
|
||||
if err != nil {
|
||||
p.setLastDialError(err)
|
||||
time.Sleep(time.Second)
|
||||
@@ -256,6 +263,7 @@ func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
|
||||
p.connsMu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
p.freeTurn()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -263,7 +271,7 @@ func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
|
||||
break
|
||||
}
|
||||
|
||||
if p.isStaleConn(cn) {
|
||||
if !p.isHealthyConn(cn) {
|
||||
_ = p.CloseConn(cn)
|
||||
continue
|
||||
}
|
||||
@@ -283,10 +291,6 @@ func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
|
||||
return newcn, nil
|
||||
}
|
||||
|
||||
func (p *ConnPool) getTurn() {
|
||||
p.queue <- struct{}{}
|
||||
}
|
||||
|
||||
func (p *ConnPool) waitTurn(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -301,7 +305,7 @@ func (p *ConnPool) waitTurn(ctx context.Context) error {
|
||||
}
|
||||
|
||||
timer := timers.Get().(*time.Timer)
|
||||
timer.Reset(p.opt.PoolTimeout)
|
||||
timer.Reset(p.cfg.PoolTimeout)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -337,7 +341,7 @@ func (p *ConnPool) popIdle() (*Conn, error) {
|
||||
}
|
||||
|
||||
var cn *Conn
|
||||
if p.opt.PoolFIFO {
|
||||
if p.cfg.PoolFIFO {
|
||||
cn = p.idleConns[0]
|
||||
copy(p.idleConns, p.idleConns[1:])
|
||||
p.idleConns = p.idleConns[:n-1]
|
||||
@@ -363,14 +367,28 @@ func (p *ConnPool) Put(ctx context.Context, cn *Conn) {
|
||||
return
|
||||
}
|
||||
|
||||
var shouldCloseConn bool
|
||||
|
||||
p.connsMu.Lock()
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
p.idleConnsLen++
|
||||
|
||||
if p.cfg.MaxIdleConns == 0 || p.idleConnsLen < p.cfg.MaxIdleConns {
|
||||
p.idleConns = append(p.idleConns, cn)
|
||||
p.idleConnsLen++
|
||||
} else {
|
||||
p.removeConn(cn)
|
||||
shouldCloseConn = true
|
||||
}
|
||||
|
||||
p.connsMu.Unlock()
|
||||
|
||||
p.freeTurn()
|
||||
|
||||
if shouldCloseConn {
|
||||
_ = p.closeConn(cn)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ConnPool) Remove(ctx context.Context, cn *Conn, reason error) {
|
||||
func (p *ConnPool) Remove(_ context.Context, cn *Conn, reason error) {
|
||||
p.removeConnWithLock(cn)
|
||||
p.freeTurn()
|
||||
_ = p.closeConn(cn)
|
||||
@@ -383,8 +401,8 @@ func (p *ConnPool) CloseConn(cn *Conn) error {
|
||||
|
||||
func (p *ConnPool) removeConnWithLock(cn *Conn) {
|
||||
p.connsMu.Lock()
|
||||
defer p.connsMu.Unlock()
|
||||
p.removeConn(cn)
|
||||
p.connsMu.Unlock()
|
||||
}
|
||||
|
||||
func (p *ConnPool) removeConn(cn *Conn) {
|
||||
@@ -395,15 +413,13 @@ func (p *ConnPool) removeConn(cn *Conn) {
|
||||
p.poolSize--
|
||||
p.checkMinIdleConns()
|
||||
}
|
||||
return
|
||||
break
|
||||
}
|
||||
}
|
||||
atomic.AddUint32(&p.stats.StaleConns, 1)
|
||||
}
|
||||
|
||||
func (p *ConnPool) closeConn(cn *Conn) error {
|
||||
if p.opt.OnClose != nil {
|
||||
_ = p.opt.OnClose(cn)
|
||||
}
|
||||
return cn.Close()
|
||||
}
|
||||
|
||||
@@ -424,14 +440,13 @@ func (p *ConnPool) IdleLen() int {
|
||||
}
|
||||
|
||||
func (p *ConnPool) Stats() *Stats {
|
||||
idleLen := p.IdleLen()
|
||||
return &Stats{
|
||||
Hits: atomic.LoadUint32(&p.stats.Hits),
|
||||
Misses: atomic.LoadUint32(&p.stats.Misses),
|
||||
Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
|
||||
|
||||
TotalConns: uint32(p.Len()),
|
||||
IdleConns: uint32(idleLen),
|
||||
IdleConns: uint32(p.IdleLen()),
|
||||
StaleConns: atomic.LoadUint32(&p.stats.StaleConns),
|
||||
}
|
||||
}
|
||||
@@ -459,7 +474,6 @@ func (p *ConnPool) Close() error {
|
||||
if !atomic.CompareAndSwapUint32(&p._closed, 0, 1) {
|
||||
return ErrClosed
|
||||
}
|
||||
close(p.closedCh)
|
||||
|
||||
var firstErr error
|
||||
p.connsMu.Lock()
|
||||
@@ -477,81 +491,20 @@ func (p *ConnPool) Close() error {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (p *ConnPool) reaper(frequency time.Duration) {
|
||||
ticker := time.NewTicker(frequency)
|
||||
defer ticker.Stop()
|
||||
func (p *ConnPool) isHealthyConn(cn *Conn) bool {
|
||||
now := time.Now()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
// It is possible that ticker and closedCh arrive together,
|
||||
// and select pseudo-randomly pick ticker case, we double
|
||||
// check here to prevent being executed after closed.
|
||||
if p.closed() {
|
||||
return
|
||||
}
|
||||
_, err := p.ReapStaleConns()
|
||||
if err != nil {
|
||||
internal.Logger.Printf(context.Background(), "ReapStaleConns failed: %s", err)
|
||||
continue
|
||||
}
|
||||
case <-p.closedCh:
|
||||
return
|
||||
}
|
||||
if p.cfg.ConnMaxLifetime > 0 && now.Sub(cn.createdAt) >= p.cfg.ConnMaxLifetime {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ConnPool) ReapStaleConns() (int, error) {
|
||||
var n int
|
||||
for {
|
||||
p.getTurn()
|
||||
|
||||
p.connsMu.Lock()
|
||||
cn := p.reapStaleConn()
|
||||
p.connsMu.Unlock()
|
||||
|
||||
p.freeTurn()
|
||||
|
||||
if cn != nil {
|
||||
_ = p.closeConn(cn)
|
||||
n++
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
atomic.AddUint32(&p.stats.StaleConns, uint32(n))
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (p *ConnPool) reapStaleConn() *Conn {
|
||||
if len(p.idleConns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cn := p.idleConns[0]
|
||||
if !p.isStaleConn(cn) {
|
||||
return nil
|
||||
}
|
||||
|
||||
p.idleConns = append(p.idleConns[:0], p.idleConns[1:]...)
|
||||
p.idleConnsLen--
|
||||
p.removeConn(cn)
|
||||
|
||||
return cn
|
||||
}
|
||||
|
||||
func (p *ConnPool) isStaleConn(cn *Conn) bool {
|
||||
if p.opt.IdleTimeout == 0 && p.opt.MaxConnAge == 0 {
|
||||
if p.cfg.ConnMaxIdleTime > 0 && now.Sub(cn.UsedAt()) >= p.cfg.ConnMaxIdleTime {
|
||||
return false
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if p.opt.IdleTimeout > 0 && now.Sub(cn.UsedAt()) >= p.opt.IdleTimeout {
|
||||
return true
|
||||
}
|
||||
if p.opt.MaxConnAge > 0 && now.Sub(cn.createdAt) >= p.opt.MaxConnAge {
|
||||
return true
|
||||
if connCheck(cn.netConn) != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
cn.SetUsedAt(now)
|
||||
return true
|
||||
}
|
||||
Generated
Vendored
Generated
Vendored
+552
@@ -0,0 +1,552 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"math/big"
|
||||
"strconv"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
// redis resp protocol data type.
|
||||
const (
|
||||
RespStatus = '+' // +<string>\r\n
|
||||
RespError = '-' // -<string>\r\n
|
||||
RespString = '$' // $<length>\r\n<bytes>\r\n
|
||||
RespInt = ':' // :<number>\r\n
|
||||
RespNil = '_' // _\r\n
|
||||
RespFloat = ',' // ,<floating-point-number>\r\n (golang float)
|
||||
RespBool = '#' // true: #t\r\n false: #f\r\n
|
||||
RespBlobError = '!' // !<length>\r\n<bytes>\r\n
|
||||
RespVerbatim = '=' // =<length>\r\nFORMAT:<bytes>\r\n
|
||||
RespBigInt = '(' // (<big number>\r\n
|
||||
RespArray = '*' // *<len>\r\n... (same as resp2)
|
||||
RespMap = '%' // %<len>\r\n(key)\r\n(value)\r\n... (golang map)
|
||||
RespSet = '~' // ~<len>\r\n... (same as Array)
|
||||
RespAttr = '|' // |<len>\r\n(key)\r\n(value)\r\n... + command reply
|
||||
RespPush = '>' // ><len>\r\n... (same as Array)
|
||||
)
|
||||
|
||||
// Not used temporarily.
|
||||
// Redis has not used these two data types for the time being, and will implement them later.
|
||||
// Streamed = "EOF:"
|
||||
// StreamedAggregated = '?'
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
const Nil = RedisError("redis: nil") // nolint:errname
|
||||
|
||||
type RedisError string
|
||||
|
||||
func (e RedisError) Error() string { return string(e) }
|
||||
|
||||
func (RedisError) RedisError() {}
|
||||
|
||||
func ParseErrorReply(line []byte) error {
|
||||
return RedisError(line[1:])
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type Reader struct {
|
||||
rd *bufio.Reader
|
||||
}
|
||||
|
||||
func NewReader(rd io.Reader) *Reader {
|
||||
return &Reader{
|
||||
rd: bufio.NewReader(rd),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) Buffered() int {
|
||||
return r.rd.Buffered()
|
||||
}
|
||||
|
||||
func (r *Reader) Peek(n int) ([]byte, error) {
|
||||
return r.rd.Peek(n)
|
||||
}
|
||||
|
||||
func (r *Reader) Reset(rd io.Reader) {
|
||||
r.rd.Reset(rd)
|
||||
}
|
||||
|
||||
// PeekReplyType returns the data type of the next response without advancing the Reader,
|
||||
// and discard the attribute type.
|
||||
func (r *Reader) PeekReplyType() (byte, error) {
|
||||
b, err := r.rd.Peek(1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if b[0] == RespAttr {
|
||||
if err = r.DiscardNext(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return r.PeekReplyType()
|
||||
}
|
||||
return b[0], nil
|
||||
}
|
||||
|
||||
// ReadLine Return a valid reply, it will check the protocol or redis error,
|
||||
// and discard the attribute type.
|
||||
func (r *Reader) ReadLine() ([]byte, error) {
|
||||
line, err := r.readLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch line[0] {
|
||||
case RespError:
|
||||
return nil, ParseErrorReply(line)
|
||||
case RespNil:
|
||||
return nil, Nil
|
||||
case RespBlobError:
|
||||
var blobErr string
|
||||
blobErr, err = r.readStringReply(line)
|
||||
if err == nil {
|
||||
err = RedisError(blobErr)
|
||||
}
|
||||
return nil, err
|
||||
case RespAttr:
|
||||
if err = r.Discard(line); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.ReadLine()
|
||||
}
|
||||
|
||||
// Compatible with RESP2
|
||||
if IsNilReply(line) {
|
||||
return nil, Nil
|
||||
}
|
||||
|
||||
return line, nil
|
||||
}
|
||||
|
||||
// readLine returns an error if:
|
||||
// - there is a pending read error;
|
||||
// - or line does not end with \r\n.
|
||||
func (r *Reader) readLine() ([]byte, error) {
|
||||
b, err := r.rd.ReadSlice('\n')
|
||||
if err != nil {
|
||||
if err != bufio.ErrBufferFull {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
full := make([]byte, len(b))
|
||||
copy(full, b)
|
||||
|
||||
b, err = r.rd.ReadBytes('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
full = append(full, b...) //nolint:makezero
|
||||
b = full
|
||||
}
|
||||
if len(b) <= 2 || b[len(b)-1] != '\n' || b[len(b)-2] != '\r' {
|
||||
return nil, fmt.Errorf("redis: invalid reply: %q", b)
|
||||
}
|
||||
return b[:len(b)-2], nil
|
||||
}
|
||||
|
||||
func (r *Reader) ReadReply() (interface{}, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch line[0] {
|
||||
case RespStatus:
|
||||
return string(line[1:]), nil
|
||||
case RespInt:
|
||||
return util.ParseInt(line[1:], 10, 64)
|
||||
case RespFloat:
|
||||
return r.readFloat(line)
|
||||
case RespBool:
|
||||
return r.readBool(line)
|
||||
case RespBigInt:
|
||||
return r.readBigInt(line)
|
||||
|
||||
case RespString:
|
||||
return r.readStringReply(line)
|
||||
case RespVerbatim:
|
||||
return r.readVerb(line)
|
||||
|
||||
case RespArray, RespSet, RespPush:
|
||||
return r.readSlice(line)
|
||||
case RespMap:
|
||||
return r.readMap(line)
|
||||
}
|
||||
return nil, fmt.Errorf("redis: can't parse %.100q", line)
|
||||
}
|
||||
|
||||
func (r *Reader) readFloat(line []byte) (float64, error) {
|
||||
v := string(line[1:])
|
||||
switch string(line[1:]) {
|
||||
case "inf":
|
||||
return math.Inf(1), nil
|
||||
case "-inf":
|
||||
return math.Inf(-1), nil
|
||||
case "nan", "-nan":
|
||||
return math.NaN(), nil
|
||||
}
|
||||
return strconv.ParseFloat(v, 64)
|
||||
}
|
||||
|
||||
func (r *Reader) readBool(line []byte) (bool, error) {
|
||||
switch string(line[1:]) {
|
||||
case "t":
|
||||
return true, nil
|
||||
case "f":
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("redis: can't parse bool reply: %q", line)
|
||||
}
|
||||
|
||||
func (r *Reader) readBigInt(line []byte) (*big.Int, error) {
|
||||
i := new(big.Int)
|
||||
if i, ok := i.SetString(string(line[1:]), 10); ok {
|
||||
return i, nil
|
||||
}
|
||||
return nil, fmt.Errorf("redis: can't parse bigInt reply: %q", line)
|
||||
}
|
||||
|
||||
func (r *Reader) readStringReply(line []byte) (string, error) {
|
||||
n, err := replyLen(line)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
b := make([]byte, n+2)
|
||||
_, err = io.ReadFull(r.rd, b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return util.BytesToString(b[:n]), nil
|
||||
}
|
||||
|
||||
func (r *Reader) readVerb(line []byte) (string, error) {
|
||||
s, err := r.readStringReply(line)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(s) < 4 || s[3] != ':' {
|
||||
return "", fmt.Errorf("redis: can't parse verbatim string reply: %q", line)
|
||||
}
|
||||
return s[4:], nil
|
||||
}
|
||||
|
||||
func (r *Reader) readSlice(line []byte) ([]interface{}, error) {
|
||||
n, err := replyLen(line)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
val := make([]interface{}, n)
|
||||
for i := 0; i < len(val); i++ {
|
||||
v, err := r.ReadReply()
|
||||
if err != nil {
|
||||
if err == Nil {
|
||||
val[i] = nil
|
||||
continue
|
||||
}
|
||||
if err, ok := err.(RedisError); ok {
|
||||
val[i] = err
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
val[i] = v
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (r *Reader) readMap(line []byte) (map[interface{}]interface{}, error) {
|
||||
n, err := replyLen(line)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := make(map[interface{}]interface{}, n)
|
||||
for i := 0; i < n; i++ {
|
||||
k, err := r.ReadReply()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err := r.ReadReply()
|
||||
if err != nil {
|
||||
if err == Nil {
|
||||
m[k] = nil
|
||||
continue
|
||||
}
|
||||
if err, ok := err.(RedisError); ok {
|
||||
m[k] = err
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
m[k] = v
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
|
||||
func (r *Reader) ReadInt() (int64, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch line[0] {
|
||||
case RespInt, RespStatus:
|
||||
return util.ParseInt(line[1:], 10, 64)
|
||||
case RespString:
|
||||
s, err := r.readStringReply(line)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return util.ParseInt([]byte(s), 10, 64)
|
||||
case RespBigInt:
|
||||
b, err := r.readBigInt(line)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !b.IsInt64() {
|
||||
return 0, fmt.Errorf("bigInt(%s) value out of range", b.String())
|
||||
}
|
||||
return b.Int64(), nil
|
||||
}
|
||||
return 0, fmt.Errorf("redis: can't parse int reply: %.100q", line)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint() (uint64, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch line[0] {
|
||||
case RespInt, RespStatus:
|
||||
return util.ParseUint(line[1:], 10, 64)
|
||||
case RespString:
|
||||
s, err := r.readStringReply(line)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return util.ParseUint([]byte(s), 10, 64)
|
||||
case RespBigInt:
|
||||
b, err := r.readBigInt(line)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if !b.IsUint64() {
|
||||
return 0, fmt.Errorf("bigInt(%s) value out of range", b.String())
|
||||
}
|
||||
return b.Uint64(), nil
|
||||
}
|
||||
return 0, fmt.Errorf("redis: can't parse uint reply: %.100q", line)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadFloat() (float64, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch line[0] {
|
||||
case RespFloat:
|
||||
return r.readFloat(line)
|
||||
case RespStatus:
|
||||
return strconv.ParseFloat(string(line[1:]), 64)
|
||||
case RespString:
|
||||
s, err := r.readStringReply(line)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.ParseFloat(s, 64)
|
||||
}
|
||||
return 0, fmt.Errorf("redis: can't parse float reply: %.100q", line)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadString() (string, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch line[0] {
|
||||
case RespStatus, RespInt, RespFloat:
|
||||
return string(line[1:]), nil
|
||||
case RespString:
|
||||
return r.readStringReply(line)
|
||||
case RespBool:
|
||||
b, err := r.readBool(line)
|
||||
return strconv.FormatBool(b), err
|
||||
case RespVerbatim:
|
||||
return r.readVerb(line)
|
||||
case RespBigInt:
|
||||
b, err := r.readBigInt(line)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
return "", fmt.Errorf("redis: can't parse reply=%.100q reading string", line)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBool() (bool, error) {
|
||||
s, err := r.ReadString()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return s == "OK" || s == "1" || s == "true", nil
|
||||
}
|
||||
|
||||
func (r *Reader) ReadSlice() ([]interface{}, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.readSlice(line)
|
||||
}
|
||||
|
||||
// ReadFixedArrayLen read fixed array length.
|
||||
func (r *Reader) ReadFixedArrayLen(fixedLen int) error {
|
||||
n, err := r.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != fixedLen {
|
||||
return fmt.Errorf("redis: got %d elements in the array, wanted %d", n, fixedLen)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadArrayLen Read and return the length of the array.
|
||||
func (r *Reader) ReadArrayLen() (int, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch line[0] {
|
||||
case RespArray, RespSet, RespPush:
|
||||
return replyLen(line)
|
||||
default:
|
||||
return 0, fmt.Errorf("redis: can't parse array/set/push reply: %.100q", line)
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFixedMapLen reads fixed map length.
|
||||
func (r *Reader) ReadFixedMapLen(fixedLen int) error {
|
||||
n, err := r.ReadMapLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != fixedLen {
|
||||
return fmt.Errorf("redis: got %d elements in the map, wanted %d", n, fixedLen)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadMapLen reads the length of the map type.
|
||||
// If responding to the array type (RespArray/RespSet/RespPush),
|
||||
// it must be a multiple of 2 and return n/2.
|
||||
// Other types will return an error.
|
||||
func (r *Reader) ReadMapLen() (int, error) {
|
||||
line, err := r.ReadLine()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
switch line[0] {
|
||||
case RespMap:
|
||||
return replyLen(line)
|
||||
case RespArray, RespSet, RespPush:
|
||||
// Some commands and RESP2 protocol may respond to array types.
|
||||
n, err := replyLen(line)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if n%2 != 0 {
|
||||
return 0, fmt.Errorf("redis: the length of the array must be a multiple of 2, got: %d", n)
|
||||
}
|
||||
return n / 2, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("redis: can't parse map reply: %.100q", line)
|
||||
}
|
||||
}
|
||||
|
||||
// DiscardNext read and discard the data represented by the next line.
|
||||
func (r *Reader) DiscardNext() error {
|
||||
line, err := r.readLine()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.Discard(line)
|
||||
}
|
||||
|
||||
// Discard the data represented by line.
|
||||
func (r *Reader) Discard(line []byte) (err error) {
|
||||
if len(line) == 0 {
|
||||
return errors.New("redis: invalid line")
|
||||
}
|
||||
switch line[0] {
|
||||
case RespStatus, RespError, RespInt, RespNil, RespFloat, RespBool, RespBigInt:
|
||||
return nil
|
||||
}
|
||||
|
||||
n, err := replyLen(line)
|
||||
if err != nil && err != Nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch line[0] {
|
||||
case RespBlobError, RespString, RespVerbatim:
|
||||
// +\r\n
|
||||
_, err = r.rd.Discard(n + 2)
|
||||
return err
|
||||
case RespArray, RespSet, RespPush:
|
||||
for i := 0; i < n; i++ {
|
||||
if err = r.DiscardNext(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
case RespMap, RespAttr:
|
||||
// Read key & value.
|
||||
for i := 0; i < n*2; i++ {
|
||||
if err = r.DiscardNext(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("redis: can't parse %.100q", line)
|
||||
}
|
||||
|
||||
func replyLen(line []byte) (n int, err error) {
|
||||
n, err = util.Atoi(line[1:])
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if n < -1 {
|
||||
return 0, fmt.Errorf("redis: invalid reply: %q", line)
|
||||
}
|
||||
|
||||
switch line[0] {
|
||||
case RespString, RespVerbatim, RespBlobError,
|
||||
RespArray, RespSet, RespPush, RespMap, RespAttr:
|
||||
if n == -1 {
|
||||
return 0, Nil
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// IsNilReply detects redis.Nil of RESP2.
|
||||
func IsNilReply(line []byte) bool {
|
||||
return len(line) == 3 &&
|
||||
(line[0] == RespString || line[0] == RespArray) &&
|
||||
line[1] == '-' && line[2] == '1'
|
||||
}
|
||||
Generated
Vendored
+6
-1
@@ -3,13 +3,15 @@ package proto
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/util"
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
// Scan parses bytes `b` to `v` with appropriate type.
|
||||
//
|
||||
//nolint:gocyclo
|
||||
func Scan(b []byte, v interface{}) error {
|
||||
switch v := v.(type) {
|
||||
@@ -115,6 +117,9 @@ func Scan(b []byte, v interface{}) error {
|
||||
return nil
|
||||
case encoding.BinaryUnmarshaler:
|
||||
return v.UnmarshalBinary(b)
|
||||
case *net.IP:
|
||||
*v = b
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf(
|
||||
"redis: can't unmarshal %T (consider implementing BinaryUnmarshaler)", v)
|
||||
Generated
Vendored
+38
-4
@@ -4,16 +4,17 @@ import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/util"
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
type writer interface {
|
||||
io.Writer
|
||||
io.ByteWriter
|
||||
// io.StringWriter
|
||||
// WriteString implement io.StringWriter.
|
||||
WriteString(s string) (n int, err error)
|
||||
}
|
||||
|
||||
@@ -34,7 +35,7 @@ func NewWriter(wr writer) *Writer {
|
||||
}
|
||||
|
||||
func (w *Writer) WriteArgs(args []interface{}) error {
|
||||
if err := w.WriteByte(ArrayReply); err != nil {
|
||||
if err := w.WriteByte(RespArray); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -64,37 +65,68 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||
return w.string("")
|
||||
case string:
|
||||
return w.string(v)
|
||||
case *string:
|
||||
return w.string(*v)
|
||||
case []byte:
|
||||
return w.bytes(v)
|
||||
case int:
|
||||
return w.int(int64(v))
|
||||
case *int:
|
||||
return w.int(int64(*v))
|
||||
case int8:
|
||||
return w.int(int64(v))
|
||||
case *int8:
|
||||
return w.int(int64(*v))
|
||||
case int16:
|
||||
return w.int(int64(v))
|
||||
case *int16:
|
||||
return w.int(int64(*v))
|
||||
case int32:
|
||||
return w.int(int64(v))
|
||||
case *int32:
|
||||
return w.int(int64(*v))
|
||||
case int64:
|
||||
return w.int(v)
|
||||
case *int64:
|
||||
return w.int(*v)
|
||||
case uint:
|
||||
return w.uint(uint64(v))
|
||||
case *uint:
|
||||
return w.uint(uint64(*v))
|
||||
case uint8:
|
||||
return w.uint(uint64(v))
|
||||
case *uint8:
|
||||
return w.uint(uint64(*v))
|
||||
case uint16:
|
||||
return w.uint(uint64(v))
|
||||
case *uint16:
|
||||
return w.uint(uint64(*v))
|
||||
case uint32:
|
||||
return w.uint(uint64(v))
|
||||
case *uint32:
|
||||
return w.uint(uint64(*v))
|
||||
case uint64:
|
||||
return w.uint(v)
|
||||
case *uint64:
|
||||
return w.uint(*v)
|
||||
case float32:
|
||||
return w.float(float64(v))
|
||||
case *float32:
|
||||
return w.float(float64(*v))
|
||||
case float64:
|
||||
return w.float(v)
|
||||
case *float64:
|
||||
return w.float(*v)
|
||||
case bool:
|
||||
if v {
|
||||
return w.int(1)
|
||||
}
|
||||
return w.int(0)
|
||||
case *bool:
|
||||
if *v {
|
||||
return w.int(1)
|
||||
}
|
||||
return w.int(0)
|
||||
case time.Time:
|
||||
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
|
||||
return w.bytes(w.numBuf)
|
||||
@@ -106,6 +138,8 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||
return err
|
||||
}
|
||||
return w.bytes(b)
|
||||
case net.IP:
|
||||
return w.bytes(v)
|
||||
default:
|
||||
return fmt.Errorf(
|
||||
"redis: can't marshal %T (implement encoding.BinaryMarshaler)", v)
|
||||
@@ -113,7 +147,7 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||
}
|
||||
|
||||
func (w *Writer) bytes(b []byte) error {
|
||||
if err := w.WriteByte(StringReply); err != nil {
|
||||
if err := w.WriteByte(RespString); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Generated
Vendored
Generated
Vendored
+1
-1
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/util"
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
func Sleep(ctx context.Context, dur time.Duration) error {
|
||||
Generated
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
//go:build appengine
|
||||
// +build appengine
|
||||
|
||||
package util
|
||||
|
||||
Generated
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
package util
|
||||
|
||||
func ToPtr[T any](v T) *T {
|
||||
return &v
|
||||
}
|
||||
Generated
Vendored
-1
@@ -1,5 +1,4 @@
|
||||
//go:build !appengine
|
||||
// +build !appengine
|
||||
|
||||
package util
|
||||
|
||||
Generated
Vendored
+1
-12
@@ -2,30 +2,21 @@ package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// ScanIterator is used to incrementally iterate over a collection of elements.
|
||||
// It's safe for concurrent use by multiple goroutines.
|
||||
type ScanIterator struct {
|
||||
mu sync.Mutex // protects Scanner and pos
|
||||
cmd *ScanCmd
|
||||
pos int
|
||||
}
|
||||
|
||||
// Err returns the last iterator error, if any.
|
||||
func (it *ScanIterator) Err() error {
|
||||
it.mu.Lock()
|
||||
err := it.cmd.Err()
|
||||
it.mu.Unlock()
|
||||
return err
|
||||
return it.cmd.Err()
|
||||
}
|
||||
|
||||
// Next advances the cursor and returns true if more values can be read.
|
||||
func (it *ScanIterator) Next(ctx context.Context) bool {
|
||||
it.mu.Lock()
|
||||
defer it.mu.Unlock()
|
||||
|
||||
// Instantly return on errors.
|
||||
if it.cmd.Err() != nil {
|
||||
return false
|
||||
@@ -68,10 +59,8 @@ func (it *ScanIterator) Next(ctx context.Context) bool {
|
||||
// Val returns the key/field at the current cursor position.
|
||||
func (it *ScanIterator) Val() string {
|
||||
var v string
|
||||
it.mu.Lock()
|
||||
if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) {
|
||||
v = it.cmd.page[it.pos-1]
|
||||
}
|
||||
it.mu.Unlock()
|
||||
return v
|
||||
}
|
||||
+599
@@ -0,0 +1,599 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
"github.com/redis/go-redis/v9/internal/util"
|
||||
)
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
type JSONCmdable interface {
|
||||
JSONArrAppend(ctx context.Context, key, path string, values ...interface{}) *IntSliceCmd
|
||||
JSONArrIndex(ctx context.Context, key, path string, value ...interface{}) *IntSliceCmd
|
||||
JSONArrIndexWithArgs(ctx context.Context, key, path string, options *JSONArrIndexArgs, value ...interface{}) *IntSliceCmd
|
||||
JSONArrInsert(ctx context.Context, key, path string, index int64, values ...interface{}) *IntSliceCmd
|
||||
JSONArrLen(ctx context.Context, key, path string) *IntSliceCmd
|
||||
JSONArrPop(ctx context.Context, key, path string, index int) *StringSliceCmd
|
||||
JSONArrTrim(ctx context.Context, key, path string) *IntSliceCmd
|
||||
JSONArrTrimWithArgs(ctx context.Context, key, path string, options *JSONArrTrimArgs) *IntSliceCmd
|
||||
JSONClear(ctx context.Context, key, path string) *IntCmd
|
||||
JSONDebugMemory(ctx context.Context, key, path string) *IntCmd
|
||||
JSONDel(ctx context.Context, key, path string) *IntCmd
|
||||
JSONForget(ctx context.Context, key, path string) *IntCmd
|
||||
JSONGet(ctx context.Context, key string, paths ...string) *JSONCmd
|
||||
JSONGetWithArgs(ctx context.Context, key string, options *JSONGetArgs, paths ...string) *JSONCmd
|
||||
JSONMerge(ctx context.Context, key, path string, value string) *StatusCmd
|
||||
JSONMSetArgs(ctx context.Context, docs []JSONSetArgs) *StatusCmd
|
||||
JSONMSet(ctx context.Context, params ...interface{}) *StatusCmd
|
||||
JSONMGet(ctx context.Context, path string, keys ...string) *JSONSliceCmd
|
||||
JSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd
|
||||
JSONObjKeys(ctx context.Context, key, path string) *SliceCmd
|
||||
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
|
||||
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
|
||||
JSONType(ctx context.Context, key, path string) *JSONSliceCmd
|
||||
}
|
||||
|
||||
type JSONSetArgs struct {
|
||||
Key string
|
||||
Path string
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type JSONArrIndexArgs struct {
|
||||
Start int
|
||||
Stop *int
|
||||
}
|
||||
|
||||
type JSONArrTrimArgs struct {
|
||||
Start int
|
||||
Stop *int
|
||||
}
|
||||
|
||||
type JSONCmd struct {
|
||||
baseCmd
|
||||
val string
|
||||
expanded []interface{}
|
||||
}
|
||||
|
||||
var _ Cmder = (*JSONCmd)(nil)
|
||||
|
||||
func newJSONCmd(ctx context.Context, args ...interface{}) *JSONCmd {
|
||||
return &JSONCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *JSONCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *JSONCmd) SetVal(val string) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *JSONCmd) Val() string {
|
||||
if len(cmd.val) == 0 && cmd.expanded != nil {
|
||||
val, err := json.Marshal(cmd.expanded)
|
||||
if err != nil {
|
||||
cmd.SetErr(err)
|
||||
return ""
|
||||
}
|
||||
return string(val)
|
||||
|
||||
} else {
|
||||
return cmd.val
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *JSONCmd) Result() (string, error) {
|
||||
return cmd.Val(), cmd.Err()
|
||||
}
|
||||
|
||||
func (cmd JSONCmd) Expanded() (interface{}, error) {
|
||||
if len(cmd.val) != 0 && cmd.expanded == nil {
|
||||
err := json.Unmarshal([]byte(cmd.val), &cmd.expanded)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return cmd.expanded, nil
|
||||
}
|
||||
|
||||
func (cmd *JSONCmd) readReply(rd *proto.Reader) error {
|
||||
// nil response from JSON.(M)GET (cmd.baseCmd.err will be "redis: nil")
|
||||
if cmd.baseCmd.Err() == Nil {
|
||||
cmd.val = ""
|
||||
return Nil
|
||||
}
|
||||
|
||||
if readType, err := rd.PeekReplyType(); err != nil {
|
||||
return err
|
||||
} else if readType == proto.RespArray {
|
||||
|
||||
size, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expanded := make([]interface{}, size)
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
if expanded[i], err = rd.ReadReply(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
cmd.expanded = expanded
|
||||
|
||||
} else {
|
||||
if str, err := rd.ReadString(); err != nil && err != Nil {
|
||||
return err
|
||||
} else if str == "" || err == Nil {
|
||||
cmd.val = ""
|
||||
} else {
|
||||
cmd.val = str
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// -------------------------------------------
|
||||
|
||||
type JSONSliceCmd struct {
|
||||
baseCmd
|
||||
val []interface{}
|
||||
}
|
||||
|
||||
func NewJSONSliceCmd(ctx context.Context, args ...interface{}) *JSONSliceCmd {
|
||||
return &JSONSliceCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *JSONSliceCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *JSONSliceCmd) SetVal(val []interface{}) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *JSONSliceCmd) Val() []interface{} {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
func (cmd *JSONSliceCmd) Result() ([]interface{}, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
|
||||
func (cmd *JSONSliceCmd) readReply(rd *proto.Reader) error {
|
||||
if cmd.baseCmd.Err() == Nil {
|
||||
cmd.val = nil
|
||||
return Nil
|
||||
}
|
||||
|
||||
if readType, err := rd.PeekReplyType(); err != nil {
|
||||
return err
|
||||
} else if readType == proto.RespArray {
|
||||
response, err := rd.ReadReply()
|
||||
if err != nil {
|
||||
return nil
|
||||
} else {
|
||||
cmd.val = response.([]interface{})
|
||||
}
|
||||
|
||||
} else {
|
||||
n, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = make([]interface{}, n)
|
||||
for i := 0; i < len(cmd.val); i++ {
|
||||
switch s, err := rd.ReadString(); {
|
||||
case err == Nil:
|
||||
cmd.val[i] = ""
|
||||
case err != nil:
|
||||
return err
|
||||
default:
|
||||
cmd.val[i] = s
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/*******************************************************************************
|
||||
*
|
||||
* IntPointerSliceCmd
|
||||
* used to represent a RedisJSON response where the result is either an integer or nil
|
||||
*
|
||||
*******************************************************************************/
|
||||
|
||||
type IntPointerSliceCmd struct {
|
||||
baseCmd
|
||||
val []*int64
|
||||
}
|
||||
|
||||
// NewIntPointerSliceCmd initialises an IntPointerSliceCmd
|
||||
func NewIntPointerSliceCmd(ctx context.Context, args ...interface{}) *IntPointerSliceCmd {
|
||||
return &IntPointerSliceCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *IntPointerSliceCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *IntPointerSliceCmd) SetVal(val []*int64) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *IntPointerSliceCmd) Val() []*int64 {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
func (cmd *IntPointerSliceCmd) Result() ([]*int64, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
|
||||
func (cmd *IntPointerSliceCmd) readReply(rd *proto.Reader) error {
|
||||
n, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = make([]*int64, n)
|
||||
|
||||
for i := 0; i < len(cmd.val); i++ {
|
||||
val, err := rd.ReadInt()
|
||||
if err != nil && err != Nil {
|
||||
return err
|
||||
} else if err != Nil {
|
||||
cmd.val[i] = &val
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// JSONArrAppend adds the provided JSON values to the end of the array at the given path.
|
||||
// For more information, see https://redis.io/commands/json.arrappend
|
||||
func (c cmdable) JSONArrAppend(ctx context.Context, key, path string, values ...interface{}) *IntSliceCmd {
|
||||
args := []interface{}{"JSON.ARRAPPEND", key, path}
|
||||
args = append(args, values...)
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONArrIndex searches for the first occurrence of the provided JSON value in the array at the given path.
|
||||
// For more information, see https://redis.io/commands/json.arrindex
|
||||
func (c cmdable) JSONArrIndex(ctx context.Context, key, path string, value ...interface{}) *IntSliceCmd {
|
||||
args := []interface{}{"JSON.ARRINDEX", key, path}
|
||||
args = append(args, value...)
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONArrIndexWithArgs searches for the first occurrence of a JSON value in an array while allowing the start and
|
||||
// stop options to be provided.
|
||||
// For more information, see https://redis.io/commands/json.arrindex
|
||||
func (c cmdable) JSONArrIndexWithArgs(ctx context.Context, key, path string, options *JSONArrIndexArgs, value ...interface{}) *IntSliceCmd {
|
||||
args := []interface{}{"JSON.ARRINDEX", key, path}
|
||||
args = append(args, value...)
|
||||
|
||||
if options != nil {
|
||||
args = append(args, options.Start)
|
||||
if options.Stop != nil {
|
||||
args = append(args, *options.Stop)
|
||||
}
|
||||
}
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONArrInsert inserts the JSON values into the array at the specified path before the index (shifts to the right).
|
||||
// For more information, see https://redis.io/commands/json.arrinsert
|
||||
func (c cmdable) JSONArrInsert(ctx context.Context, key, path string, index int64, values ...interface{}) *IntSliceCmd {
|
||||
args := []interface{}{"JSON.ARRINSERT", key, path, index}
|
||||
args = append(args, values...)
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONArrLen reports the length of the JSON array at the specified path in the given key.
|
||||
// For more information, see https://redis.io/commands/json.arrlen
|
||||
func (c cmdable) JSONArrLen(ctx context.Context, key, path string) *IntSliceCmd {
|
||||
args := []interface{}{"JSON.ARRLEN", key, path}
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONArrPop removes and returns an element from the specified index in the array.
|
||||
// For more information, see https://redis.io/commands/json.arrpop
|
||||
func (c cmdable) JSONArrPop(ctx context.Context, key, path string, index int) *StringSliceCmd {
|
||||
args := []interface{}{"JSON.ARRPOP", key, path, index}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONArrTrim trims an array to contain only the specified inclusive range of elements.
|
||||
// For more information, see https://redis.io/commands/json.arrtrim
|
||||
func (c cmdable) JSONArrTrim(ctx context.Context, key, path string) *IntSliceCmd {
|
||||
args := []interface{}{"JSON.ARRTRIM", key, path}
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONArrTrimWithArgs trims an array to contain only the specified inclusive range of elements.
|
||||
// For more information, see https://redis.io/commands/json.arrtrim
|
||||
func (c cmdable) JSONArrTrimWithArgs(ctx context.Context, key, path string, options *JSONArrTrimArgs) *IntSliceCmd {
|
||||
args := []interface{}{"JSON.ARRTRIM", key, path}
|
||||
|
||||
if options != nil {
|
||||
args = append(args, options.Start)
|
||||
|
||||
if options.Stop != nil {
|
||||
args = append(args, *options.Stop)
|
||||
}
|
||||
}
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONClear clears container values (arrays/objects) and sets numeric values to 0.
|
||||
// For more information, see https://redis.io/commands/json.clear
|
||||
func (c cmdable) JSONClear(ctx context.Context, key, path string) *IntCmd {
|
||||
args := []interface{}{"JSON.CLEAR", key, path}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONDebugMemory reports a value's memory usage in bytes (unimplemented)
|
||||
// For more information, see https://redis.io/commands/json.debug-memory
|
||||
func (c cmdable) JSONDebugMemory(ctx context.Context, key, path string) *IntCmd {
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
// JSONDel deletes a value.
|
||||
// For more information, see https://redis.io/commands/json.del
|
||||
func (c cmdable) JSONDel(ctx context.Context, key, path string) *IntCmd {
|
||||
args := []interface{}{"JSON.DEL", key, path}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONForget deletes a value.
|
||||
// For more information, see https://redis.io/commands/json.forget
|
||||
func (c cmdable) JSONForget(ctx context.Context, key, path string) *IntCmd {
|
||||
args := []interface{}{"JSON.FORGET", key, path}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONGet returns the value at path in JSON serialized form. JSON.GET returns an
|
||||
// array of strings. This function parses out the wrapping array but leaves the
|
||||
// internal strings unprocessed by default (see Val())
|
||||
// For more information - https://redis.io/commands/json.get/
|
||||
func (c cmdable) JSONGet(ctx context.Context, key string, paths ...string) *JSONCmd {
|
||||
args := make([]interface{}, len(paths)+2)
|
||||
args[0] = "JSON.GET"
|
||||
args[1] = key
|
||||
for n, path := range paths {
|
||||
args[n+2] = path
|
||||
}
|
||||
cmd := newJSONCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type JSONGetArgs struct {
|
||||
Indent string
|
||||
Newline string
|
||||
Space string
|
||||
}
|
||||
|
||||
// JSONGetWithArgs - Retrieves the value of a key from a JSON document.
|
||||
// This function also allows for specifying additional options such as:
|
||||
// Indention, NewLine and Space
|
||||
// For more information - https://redis.io/commands/json.get/
|
||||
func (c cmdable) JSONGetWithArgs(ctx context.Context, key string, options *JSONGetArgs, paths ...string) *JSONCmd {
|
||||
args := []interface{}{"JSON.GET", key}
|
||||
if options != nil {
|
||||
if options.Indent != "" {
|
||||
args = append(args, "INDENT", options.Indent)
|
||||
}
|
||||
if options.Newline != "" {
|
||||
args = append(args, "NEWLINE", options.Newline)
|
||||
}
|
||||
if options.Space != "" {
|
||||
args = append(args, "SPACE", options.Space)
|
||||
}
|
||||
for _, path := range paths {
|
||||
args = append(args, path)
|
||||
}
|
||||
}
|
||||
cmd := newJSONCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONMerge merges a given JSON value into matching paths.
|
||||
// For more information, see https://redis.io/commands/json.merge
|
||||
func (c cmdable) JSONMerge(ctx context.Context, key, path string, value string) *StatusCmd {
|
||||
args := []interface{}{"JSON.MERGE", key, path, value}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONMGet returns the values at the specified path from multiple key arguments.
|
||||
// Note - the arguments are reversed when compared with `JSON.MGET` as we want
|
||||
// to follow the pattern of having the last argument be variable.
|
||||
// For more information, see https://redis.io/commands/json.mget
|
||||
func (c cmdable) JSONMGet(ctx context.Context, path string, keys ...string) *JSONSliceCmd {
|
||||
args := make([]interface{}, len(keys)+1)
|
||||
args[0] = "JSON.MGET"
|
||||
for n, key := range keys {
|
||||
args[n+1] = key
|
||||
}
|
||||
args = append(args, path)
|
||||
cmd := NewJSONSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONMSetArgs sets or updates one or more JSON values according to the specified key-path-value triplets.
|
||||
// For more information, see https://redis.io/commands/json.mset
|
||||
func (c cmdable) JSONMSetArgs(ctx context.Context, docs []JSONSetArgs) *StatusCmd {
|
||||
args := []interface{}{"JSON.MSET"}
|
||||
for _, doc := range docs {
|
||||
args = append(args, doc.Key, doc.Path, doc.Value)
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) JSONMSet(ctx context.Context, params ...interface{}) *StatusCmd {
|
||||
args := []interface{}{"JSON.MSET"}
|
||||
args = append(args, params...)
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONNumIncrBy increments the number value stored at the specified path by the provided number.
|
||||
// For more information, see https://redis.io/commands/json.numincreby
|
||||
func (c cmdable) JSONNumIncrBy(ctx context.Context, key, path string, value float64) *JSONCmd {
|
||||
args := []interface{}{"JSON.NUMINCRBY", key, path, value}
|
||||
cmd := newJSONCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONObjKeys returns the keys in the object that's referenced by the specified path.
|
||||
// For more information, see https://redis.io/commands/json.objkeys
|
||||
func (c cmdable) JSONObjKeys(ctx context.Context, key, path string) *SliceCmd {
|
||||
args := []interface{}{"JSON.OBJKEYS", key, path}
|
||||
cmd := NewSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONObjLen reports the number of keys in the JSON object at the specified path in the given key.
|
||||
// For more information, see https://redis.io/commands/json.objlen
|
||||
func (c cmdable) JSONObjLen(ctx context.Context, key, path string) *IntPointerSliceCmd {
|
||||
args := []interface{}{"JSON.OBJLEN", key, path}
|
||||
cmd := NewIntPointerSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONSet sets the JSON value at the given path in the given key. The value must be something that
|
||||
// can be marshaled to JSON (using encoding/JSON) unless the argument is a string or a []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) JSONSet(ctx context.Context, key, path string, value interface{}) *StatusCmd {
|
||||
return c.JSONSetMode(ctx, key, path, value, "")
|
||||
}
|
||||
|
||||
// JSONSetMode sets the JSON value at the given path in the given key and allows the mode to be set
|
||||
// (the mode value must be "XX" or "NX"). 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) JSONSetMode(ctx context.Context, key, path string, value interface{}, mode string) *StatusCmd {
|
||||
var bytes []byte
|
||||
var err error
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
bytes = []byte(v)
|
||||
case []byte:
|
||||
bytes = v
|
||||
default:
|
||||
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")
|
||||
}
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
if err != nil {
|
||||
cmd.SetErr(err)
|
||||
} else {
|
||||
_ = c(ctx, cmd)
|
||||
}
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONStrAppend appends the JSON-string values to the string at the specified path.
|
||||
// For more information, see https://redis.io/commands/json.strappend
|
||||
func (c cmdable) JSONStrAppend(ctx context.Context, key, path, value string) *IntPointerSliceCmd {
|
||||
args := []interface{}{"JSON.STRAPPEND", key, path, value}
|
||||
cmd := NewIntPointerSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONStrLen reports the length of the JSON String at the specified path in the given key.
|
||||
// For more information, see https://redis.io/commands/json.strlen
|
||||
func (c cmdable) JSONStrLen(ctx context.Context, key, path string) *IntPointerSliceCmd {
|
||||
args := []interface{}{"JSON.STRLEN", key, path}
|
||||
cmd := NewIntPointerSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONToggle toggles a Boolean value stored at the specified path.
|
||||
// For more information, see https://redis.io/commands/json.toggle
|
||||
func (c cmdable) JSONToggle(ctx context.Context, key, path string) *IntPointerSliceCmd {
|
||||
args := []interface{}{"JSON.TOGGLE", key, path}
|
||||
cmd := NewIntPointerSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// JSONType reports the type of JSON value at the specified path.
|
||||
// For more information, see https://redis.io/commands/json.type
|
||||
func (c cmdable) JSONType(ctx context.Context, key, path string) *JSONSliceCmd {
|
||||
args := []interface{}{"JSON.TYPE", key, path}
|
||||
cmd := NewJSONSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+289
@@ -0,0 +1,289 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ListCmdable interface {
|
||||
BLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd
|
||||
BLMPop(ctx context.Context, timeout time.Duration, direction string, count int64, keys ...string) *KeyValuesCmd
|
||||
BRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd
|
||||
BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd
|
||||
LIndex(ctx context.Context, key string, index int64) *StringCmd
|
||||
LInsert(ctx context.Context, key, op string, pivot, value interface{}) *IntCmd
|
||||
LInsertBefore(ctx context.Context, key string, pivot, value interface{}) *IntCmd
|
||||
LInsertAfter(ctx context.Context, key string, pivot, value interface{}) *IntCmd
|
||||
LLen(ctx context.Context, key string) *IntCmd
|
||||
LMPop(ctx context.Context, direction string, count int64, keys ...string) *KeyValuesCmd
|
||||
LPop(ctx context.Context, key string) *StringCmd
|
||||
LPopCount(ctx context.Context, key string, count int) *StringSliceCmd
|
||||
LPos(ctx context.Context, key string, value string, args LPosArgs) *IntCmd
|
||||
LPosCount(ctx context.Context, key string, value string, count int64, args LPosArgs) *IntSliceCmd
|
||||
LPush(ctx context.Context, key string, values ...interface{}) *IntCmd
|
||||
LPushX(ctx context.Context, key string, values ...interface{}) *IntCmd
|
||||
LRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd
|
||||
LRem(ctx context.Context, key string, count int64, value interface{}) *IntCmd
|
||||
LSet(ctx context.Context, key string, index int64, value interface{}) *StatusCmd
|
||||
LTrim(ctx context.Context, key string, start, stop int64) *StatusCmd
|
||||
RPop(ctx context.Context, key string) *StringCmd
|
||||
RPopCount(ctx context.Context, key string, count int) *StringSliceCmd
|
||||
RPopLPush(ctx context.Context, source, destination string) *StringCmd
|
||||
RPush(ctx context.Context, key string, values ...interface{}) *IntCmd
|
||||
RPushX(ctx context.Context, key string, values ...interface{}) *IntCmd
|
||||
LMove(ctx context.Context, source, destination, srcpos, destpos string) *StringCmd
|
||||
BLMove(ctx context.Context, source, destination, srcpos, destpos string, timeout time.Duration) *StringCmd
|
||||
}
|
||||
|
||||
func (c cmdable) BLPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd {
|
||||
args := make([]interface{}, 1+len(keys)+1)
|
||||
args[0] = "blpop"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
args[len(args)-1] = formatSec(ctx, timeout)
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) BLMPop(ctx context.Context, timeout time.Duration, direction string, count int64, keys ...string) *KeyValuesCmd {
|
||||
args := make([]interface{}, 3+len(keys), 6+len(keys))
|
||||
args[0] = "blmpop"
|
||||
args[1] = formatSec(ctx, timeout)
|
||||
args[2] = len(keys)
|
||||
for i, key := range keys {
|
||||
args[3+i] = key
|
||||
}
|
||||
args = append(args, strings.ToLower(direction), "count", count)
|
||||
cmd := NewKeyValuesCmd(ctx, args...)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) BRPop(ctx context.Context, timeout time.Duration, keys ...string) *StringSliceCmd {
|
||||
args := make([]interface{}, 1+len(keys)+1)
|
||||
args[0] = "brpop"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
args[len(keys)+1] = formatSec(ctx, timeout)
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) BRPopLPush(ctx context.Context, source, destination string, timeout time.Duration) *StringCmd {
|
||||
cmd := NewStringCmd(
|
||||
ctx,
|
||||
"brpoplpush",
|
||||
source,
|
||||
destination,
|
||||
formatSec(ctx, timeout),
|
||||
)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LIndex(ctx context.Context, key string, index int64) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "lindex", key, index)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// LMPop Pops one or more elements from the first non-empty list key from the list of provided key names.
|
||||
// direction: left or right, count: > 0
|
||||
// example: client.LMPop(ctx, "left", 3, "key1", "key2")
|
||||
func (c cmdable) LMPop(ctx context.Context, direction string, count int64, keys ...string) *KeyValuesCmd {
|
||||
args := make([]interface{}, 2+len(keys), 5+len(keys))
|
||||
args[0] = "lmpop"
|
||||
args[1] = len(keys)
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
}
|
||||
args = append(args, strings.ToLower(direction), "count", count)
|
||||
cmd := NewKeyValuesCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LInsert(ctx context.Context, key, op string, pivot, value interface{}) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "linsert", key, op, pivot, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LInsertBefore(ctx context.Context, key string, pivot, value interface{}) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "linsert", key, "before", pivot, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LInsertAfter(ctx context.Context, key string, pivot, value interface{}) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "linsert", key, "after", pivot, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LLen(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "llen", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LPop(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "lpop", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LPopCount(ctx context.Context, key string, count int) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "lpop", key, count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type LPosArgs struct {
|
||||
Rank, MaxLen int64
|
||||
}
|
||||
|
||||
func (c cmdable) LPos(ctx context.Context, key string, value string, a LPosArgs) *IntCmd {
|
||||
args := []interface{}{"lpos", key, value}
|
||||
if a.Rank != 0 {
|
||||
args = append(args, "rank", a.Rank)
|
||||
}
|
||||
if a.MaxLen != 0 {
|
||||
args = append(args, "maxlen", a.MaxLen)
|
||||
}
|
||||
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LPosCount(ctx context.Context, key string, value string, count int64, a LPosArgs) *IntSliceCmd {
|
||||
args := []interface{}{"lpos", key, value, "count", count}
|
||||
if a.Rank != 0 {
|
||||
args = append(args, "rank", a.Rank)
|
||||
}
|
||||
if a.MaxLen != 0 {
|
||||
args = append(args, "maxlen", a.MaxLen)
|
||||
}
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LPush(ctx context.Context, key string, values ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "lpush"
|
||||
args[1] = key
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LPushX(ctx context.Context, key string, values ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "lpushx"
|
||||
args[1] = key
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(
|
||||
ctx,
|
||||
"lrange",
|
||||
key,
|
||||
start,
|
||||
stop,
|
||||
)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LRem(ctx context.Context, key string, count int64, value interface{}) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "lrem", key, count, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LSet(ctx context.Context, key string, index int64, value interface{}) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "lset", key, index, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LTrim(ctx context.Context, key string, start, stop int64) *StatusCmd {
|
||||
cmd := NewStatusCmd(
|
||||
ctx,
|
||||
"ltrim",
|
||||
key,
|
||||
start,
|
||||
stop,
|
||||
)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RPop(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "rpop", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RPopCount(ctx context.Context, key string, count int) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "rpop", key, count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RPopLPush(ctx context.Context, source, destination string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "rpoplpush", source, destination)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RPush(ctx context.Context, key string, values ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "rpush"
|
||||
args[1] = key
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) RPushX(ctx context.Context, key string, values ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(values))
|
||||
args[0] = "rpushx"
|
||||
args[1] = key
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LMove(ctx context.Context, source, destination, srcpos, destpos string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "lmove", source, destination, srcpos, destpos)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) BLMove(
|
||||
ctx context.Context, source, destination, srcpos, destpos string, timeout time.Duration,
|
||||
) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "blmove", source, destination, srcpos, destpos, formatSec(ctx, timeout))
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
Generated
Vendored
+179
-90
@@ -13,7 +13,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
)
|
||||
|
||||
// Limiter is the interface of a rate limiter or a circuit breaker.
|
||||
@@ -27,7 +27,7 @@ type Limiter interface {
|
||||
ReportResult(result error)
|
||||
}
|
||||
|
||||
// Options keeps the settings to setup redis connection.
|
||||
// Options keeps the settings to set up redis connection.
|
||||
type Options struct {
|
||||
// The network type, either tcp or unix.
|
||||
// Default is tcp.
|
||||
@@ -35,6 +35,9 @@ type Options struct {
|
||||
// host:port address.
|
||||
Addr string
|
||||
|
||||
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
|
||||
ClientName string
|
||||
|
||||
// Dialer creates new network connection and has priority over
|
||||
// Network and Addr options.
|
||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
@@ -42,6 +45,9 @@ type Options struct {
|
||||
// Hook that is called when new connection is established.
|
||||
OnConnect func(ctx context.Context, cn *Conn) error
|
||||
|
||||
// Protocol 2 or 3. Use the version to negotiate RESP version with redis-server.
|
||||
// Default is 3.
|
||||
Protocol int
|
||||
// Use the specified Username to authenticate the current connection
|
||||
// with one of the connections defined in the ACL list when connecting
|
||||
// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
|
||||
@@ -51,6 +57,9 @@ type Options struct {
|
||||
// or the User Password when connecting to a Redis 6.0 instance, or greater,
|
||||
// that is using the Redis ACL system.
|
||||
Password string
|
||||
// CredentialsProvider allows the username and password to be updated
|
||||
// before reconnecting. It should return the current username and password.
|
||||
CredentialsProvider func() (username string, password string)
|
||||
|
||||
// Database to be selected after connecting to the server.
|
||||
DB int
|
||||
@@ -69,49 +78,75 @@ type Options struct {
|
||||
// Default is 5 seconds.
|
||||
DialTimeout time.Duration
|
||||
// Timeout for socket reads. If reached, commands will fail
|
||||
// with a timeout instead of blocking. Use value -1 for no timeout and 0 for default.
|
||||
// Default is 3 seconds.
|
||||
// with a timeout instead of blocking. Supported values:
|
||||
// - `0` - default timeout (3 seconds).
|
||||
// - `-1` - no timeout (block indefinitely).
|
||||
// - `-2` - disables SetReadDeadline calls completely.
|
||||
ReadTimeout time.Duration
|
||||
// Timeout for socket writes. If reached, commands will fail
|
||||
// with a timeout instead of blocking.
|
||||
// Default is ReadTimeout.
|
||||
// with a timeout instead of blocking. Supported values:
|
||||
// - `0` - default timeout (3 seconds).
|
||||
// - `-1` - no timeout (block indefinitely).
|
||||
// - `-2` - disables SetWriteDeadline calls completely.
|
||||
WriteTimeout time.Duration
|
||||
// ContextTimeoutEnabled controls whether the client respects context timeouts and deadlines.
|
||||
// See https://redis.uptrace.dev/guide/go-redis-debugging.html#timeouts
|
||||
ContextTimeoutEnabled bool
|
||||
|
||||
// Type of connection pool.
|
||||
// true for FIFO pool, false for LIFO pool.
|
||||
// Note that fifo has higher overhead compared to lifo.
|
||||
// Note that FIFO has slightly higher overhead compared to LIFO,
|
||||
// but it helps closing idle connections faster reducing the pool size.
|
||||
PoolFIFO bool
|
||||
// Maximum number of socket connections.
|
||||
// Base number of socket connections.
|
||||
// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||
// If there is not enough connections in the pool, new connections will be allocated in excess of PoolSize,
|
||||
// you can limit it through MaxActiveConns
|
||||
PoolSize int
|
||||
// Minimum number of idle connections which is useful when establishing
|
||||
// new connection is slow.
|
||||
MinIdleConns int
|
||||
// Connection age at which client retires (closes) the connection.
|
||||
// Default is to not close aged connections.
|
||||
MaxConnAge time.Duration
|
||||
// Amount of time client waits for connection if all connections
|
||||
// are busy before returning an error.
|
||||
// Default is ReadTimeout + 1 second.
|
||||
PoolTimeout time.Duration
|
||||
// Amount of time after which client closes idle connections.
|
||||
// Minimum number of idle connections which is useful when establishing
|
||||
// new connection is slow.
|
||||
// Default is 0. the idle connections are not closed by default.
|
||||
MinIdleConns int
|
||||
// Maximum number of idle connections.
|
||||
// Default is 0. the idle connections are not closed by default.
|
||||
MaxIdleConns int
|
||||
// Maximum number of connections allocated by the pool at a given time.
|
||||
// When zero, there is no limit on the number of connections in the pool.
|
||||
MaxActiveConns int
|
||||
// ConnMaxIdleTime is the maximum amount of time a connection may be idle.
|
||||
// Should be less than server's timeout.
|
||||
// Default is 5 minutes. -1 disables idle timeout check.
|
||||
IdleTimeout time.Duration
|
||||
// Frequency of idle checks made by idle connections reaper.
|
||||
// Default is 1 minute. -1 disables idle connections reaper,
|
||||
// but idle connections are still discarded by the client
|
||||
// if IdleTimeout is set.
|
||||
IdleCheckFrequency time.Duration
|
||||
//
|
||||
// Expired connections may be closed lazily before reuse.
|
||||
// If d <= 0, connections are not closed due to a connection's idle time.
|
||||
//
|
||||
// Default is 30 minutes. -1 disables idle timeout check.
|
||||
ConnMaxIdleTime time.Duration
|
||||
// ConnMaxLifetime is the maximum amount of time a connection may be reused.
|
||||
//
|
||||
// Expired connections may be closed lazily before reuse.
|
||||
// If <= 0, connections are not closed due to a connection's age.
|
||||
//
|
||||
// Default is to not close idle connections.
|
||||
ConnMaxLifetime time.Duration
|
||||
|
||||
// Enables read only queries on slave nodes.
|
||||
readOnly bool
|
||||
|
||||
// TLS Config to use. When set TLS will be negotiated.
|
||||
// TLS Config to use. When set, TLS will be negotiated.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Limiter interface used to implemented circuit breaker or rate limiter.
|
||||
// Limiter interface used to implement circuit breaker or rate limiter.
|
||||
Limiter Limiter
|
||||
|
||||
// Enables read only queries on slave/follower nodes.
|
||||
readOnly bool
|
||||
|
||||
// Disable set-lib on connect. Default is false.
|
||||
DisableIndentity bool
|
||||
|
||||
// Add suffix to client name. Default is empty.
|
||||
IdentitySuffix string
|
||||
}
|
||||
|
||||
func (opt *Options) init() {
|
||||
@@ -129,40 +164,36 @@ func (opt *Options) init() {
|
||||
opt.DialTimeout = 5 * time.Second
|
||||
}
|
||||
if opt.Dialer == nil {
|
||||
opt.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
netDialer := &net.Dialer{
|
||||
Timeout: opt.DialTimeout,
|
||||
KeepAlive: 5 * time.Minute,
|
||||
}
|
||||
if opt.TLSConfig == nil {
|
||||
return netDialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
return tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig)
|
||||
}
|
||||
opt.Dialer = NewDialer(opt)
|
||||
}
|
||||
if opt.PoolSize == 0 {
|
||||
opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
|
||||
}
|
||||
switch opt.ReadTimeout {
|
||||
case -2:
|
||||
opt.ReadTimeout = -1
|
||||
case -1:
|
||||
opt.ReadTimeout = 0
|
||||
case 0:
|
||||
opt.ReadTimeout = 3 * time.Second
|
||||
}
|
||||
switch opt.WriteTimeout {
|
||||
case -2:
|
||||
opt.WriteTimeout = -1
|
||||
case -1:
|
||||
opt.WriteTimeout = 0
|
||||
case 0:
|
||||
opt.WriteTimeout = opt.ReadTimeout
|
||||
}
|
||||
if opt.PoolTimeout == 0 {
|
||||
opt.PoolTimeout = opt.ReadTimeout + time.Second
|
||||
if opt.ReadTimeout > 0 {
|
||||
opt.PoolTimeout = opt.ReadTimeout + time.Second
|
||||
} else {
|
||||
opt.PoolTimeout = 30 * time.Second
|
||||
}
|
||||
}
|
||||
if opt.IdleTimeout == 0 {
|
||||
opt.IdleTimeout = 5 * time.Minute
|
||||
}
|
||||
if opt.IdleCheckFrequency == 0 {
|
||||
opt.IdleCheckFrequency = time.Minute
|
||||
if opt.ConnMaxIdleTime == 0 {
|
||||
opt.ConnMaxIdleTime = 30 * time.Minute
|
||||
}
|
||||
|
||||
if opt.MaxRetries == -1 {
|
||||
@@ -189,36 +220,57 @@ func (opt *Options) clone() *Options {
|
||||
return &clone
|
||||
}
|
||||
|
||||
// NewDialer returns a function that will be used as the default dialer
|
||||
// when none is specified in Options.Dialer.
|
||||
func NewDialer(opt *Options) func(context.Context, string, string) (net.Conn, error) {
|
||||
return func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
netDialer := &net.Dialer{
|
||||
Timeout: opt.DialTimeout,
|
||||
KeepAlive: 5 * time.Minute,
|
||||
}
|
||||
if opt.TLSConfig == nil {
|
||||
return netDialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
return tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseURL parses an URL into Options that can be used to connect to Redis.
|
||||
// Scheme is required.
|
||||
// There are two connection types: by tcp socket and by unix socket.
|
||||
// Tcp connection:
|
||||
// redis://<user>:<password>@<host>:<port>/<db_number>
|
||||
//
|
||||
// redis://<user>:<password>@<host>:<port>/<db_number>
|
||||
//
|
||||
// Unix connection:
|
||||
// unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
|
||||
//
|
||||
// unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
|
||||
//
|
||||
// Most Option fields can be set using query parameters, with the following restrictions:
|
||||
// - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
|
||||
// - only scalar type fields are supported (bool, int, time.Duration)
|
||||
// - for time.Duration fields, values must be a valid input for time.ParseDuration();
|
||||
// additionally a plain integer as value (i.e. without unit) is intepreted as seconds
|
||||
// - to disable a duration field, use value less than or equal to 0; to use the default
|
||||
// value, leave the value blank or remove the parameter
|
||||
// - only the last value is interpreted if a parameter is given multiple times
|
||||
// - fields "network", "addr", "username" and "password" can only be set using other
|
||||
// URL attributes (scheme, host, userinfo, resp.), query paremeters using these
|
||||
// names will be treated as unknown parameters
|
||||
// - unknown parameter names will result in an error
|
||||
// - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
|
||||
// - only scalar type fields are supported (bool, int, time.Duration)
|
||||
// - for time.Duration fields, values must be a valid input for time.ParseDuration();
|
||||
// additionally a plain integer as value (i.e. without unit) is intepreted as seconds
|
||||
// - to disable a duration field, use value less than or equal to 0; to use the default
|
||||
// value, leave the value blank or remove the parameter
|
||||
// - only the last value is interpreted if a parameter is given multiple times
|
||||
// - fields "network", "addr", "username" and "password" can only be set using other
|
||||
// URL attributes (scheme, host, userinfo, resp.), query paremeters using these
|
||||
// names will be treated as unknown parameters
|
||||
// - unknown parameter names will result in an error
|
||||
//
|
||||
// Examples:
|
||||
// redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
|
||||
// is equivalent to:
|
||||
// &Options{
|
||||
// Network: "tcp",
|
||||
// Addr: "localhost:6789",
|
||||
// DB: 1, // path "/3" was overridden by "&db=1"
|
||||
// DialTimeout: 3 * time.Second, // no time unit = seconds
|
||||
// ReadTimeout: 6 * time.Second,
|
||||
// MaxRetries: 2,
|
||||
// }
|
||||
//
|
||||
// redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
|
||||
// is equivalent to:
|
||||
// &Options{
|
||||
// Network: "tcp",
|
||||
// Addr: "localhost:6789",
|
||||
// DB: 1, // path "/3" was overridden by "&db=1"
|
||||
// DialTimeout: 3 * time.Second, // no time unit = seconds
|
||||
// ReadTimeout: 6 * time.Second,
|
||||
// MaxRetries: 2,
|
||||
// }
|
||||
func ParseURL(redisURL string) (*Options, error) {
|
||||
u, err := url.Parse(redisURL)
|
||||
if err != nil {
|
||||
@@ -240,16 +292,7 @@ func setupTCPConn(u *url.URL) (*Options, error) {
|
||||
|
||||
o.Username, o.Password = getUserPassword(u)
|
||||
|
||||
h, p, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
h = u.Host
|
||||
}
|
||||
if h == "" {
|
||||
h = "localhost"
|
||||
}
|
||||
if p == "" {
|
||||
p = "6379"
|
||||
}
|
||||
h, p := getHostPortWithDefaults(u)
|
||||
o.Addr = net.JoinHostPort(h, p)
|
||||
|
||||
f := strings.FieldsFunc(u.Path, func(r rune) bool {
|
||||
@@ -259,6 +302,7 @@ func setupTCPConn(u *url.URL) (*Options, error) {
|
||||
case 0:
|
||||
o.DB = 0
|
||||
case 1:
|
||||
var err error
|
||||
if o.DB, err = strconv.Atoi(f[0]); err != nil {
|
||||
return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
|
||||
}
|
||||
@@ -267,12 +311,32 @@ func setupTCPConn(u *url.URL) (*Options, error) {
|
||||
}
|
||||
|
||||
if u.Scheme == "rediss" {
|
||||
o.TLSConfig = &tls.Config{ServerName: h}
|
||||
o.TLSConfig = &tls.Config{
|
||||
ServerName: h,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
return setupConnParams(u, o)
|
||||
}
|
||||
|
||||
// getHostPortWithDefaults is a helper function that splits the url into
|
||||
// a host and a port. If the host is missing, it defaults to localhost
|
||||
// and if the port is missing, it defaults to 6379.
|
||||
func getHostPortWithDefaults(u *url.URL) (string, string) {
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
host = u.Host
|
||||
}
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
if port == "" {
|
||||
port = "6379"
|
||||
}
|
||||
return host, port
|
||||
}
|
||||
|
||||
func setupUnixConn(u *url.URL) (*Options, error) {
|
||||
o := &Options{
|
||||
Network: "unix",
|
||||
@@ -291,6 +355,10 @@ type queryOptions struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (o *queryOptions) has(name string) bool {
|
||||
return len(o.q[name]) > 0
|
||||
}
|
||||
|
||||
func (o *queryOptions) string(name string) string {
|
||||
vs := o.q[name]
|
||||
if len(vs) == 0 {
|
||||
@@ -300,6 +368,12 @@ func (o *queryOptions) string(name string) string {
|
||||
return vs[len(vs)-1]
|
||||
}
|
||||
|
||||
func (o *queryOptions) strings(name string) []string {
|
||||
vs := o.q[name]
|
||||
delete(o.q, name)
|
||||
return vs
|
||||
}
|
||||
|
||||
func (o *queryOptions) int(name string) int {
|
||||
s := o.string(name)
|
||||
if s == "" {
|
||||
@@ -377,6 +451,8 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) {
|
||||
o.DB = db
|
||||
}
|
||||
|
||||
o.Protocol = q.int("protocol")
|
||||
o.ClientName = q.string("client_name")
|
||||
o.MaxRetries = q.int("max_retries")
|
||||
o.MinRetryBackoff = q.duration("min_retry_backoff")
|
||||
o.MaxRetryBackoff = q.duration("max_retry_backoff")
|
||||
@@ -385,11 +461,20 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) {
|
||||
o.WriteTimeout = q.duration("write_timeout")
|
||||
o.PoolFIFO = q.bool("pool_fifo")
|
||||
o.PoolSize = q.int("pool_size")
|
||||
o.MinIdleConns = q.int("min_idle_conns")
|
||||
o.MaxConnAge = q.duration("max_conn_age")
|
||||
o.PoolTimeout = q.duration("pool_timeout")
|
||||
o.IdleTimeout = q.duration("idle_timeout")
|
||||
o.IdleCheckFrequency = q.duration("idle_check_frequency")
|
||||
o.MinIdleConns = q.int("min_idle_conns")
|
||||
o.MaxIdleConns = q.int("max_idle_conns")
|
||||
o.MaxActiveConns = q.int("max_active_conns")
|
||||
if q.has("conn_max_idle_time") {
|
||||
o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
|
||||
} else {
|
||||
o.ConnMaxIdleTime = q.duration("idle_timeout")
|
||||
}
|
||||
if q.has("conn_max_lifetime") {
|
||||
o.ConnMaxLifetime = q.duration("conn_max_lifetime")
|
||||
} else {
|
||||
o.ConnMaxLifetime = q.duration("max_conn_age")
|
||||
}
|
||||
if q.err != nil {
|
||||
return nil, q.err
|
||||
}
|
||||
@@ -413,17 +498,21 @@ func getUserPassword(u *url.URL) (string, string) {
|
||||
return user, password
|
||||
}
|
||||
|
||||
func newConnPool(opt *Options) *pool.ConnPool {
|
||||
func newConnPool(
|
||||
opt *Options,
|
||||
dialer func(ctx context.Context, network, addr string) (net.Conn, error),
|
||||
) *pool.ConnPool {
|
||||
return pool.NewConnPool(&pool.Options{
|
||||
Dialer: func(ctx context.Context) (net.Conn, error) {
|
||||
return opt.Dialer(ctx, opt.Network, opt.Addr)
|
||||
return dialer(ctx, opt.Network, opt.Addr)
|
||||
},
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxConnAge: opt.MaxConnAge,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
IdleTimeout: opt.IdleTimeout,
|
||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxIdleConns: opt.MaxIdleConns,
|
||||
MaxActiveConns: opt.MaxActiveConns,
|
||||
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||
})
|
||||
}
|
||||
Generated
Vendored
+365
-204
@@ -6,17 +6,19 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal"
|
||||
"github.com/go-redis/redis/v8/internal/hashtag"
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/go-redis/redis/v8/internal/proto"
|
||||
"github.com/go-redis/redis/v8/internal/rand"
|
||||
"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 errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes")
|
||||
@@ -27,6 +29,9 @@ type ClusterOptions struct {
|
||||
// A seed list of host:port addresses of cluster nodes.
|
||||
Addrs []string
|
||||
|
||||
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
|
||||
ClientName string
|
||||
|
||||
// NewClient creates a cluster node client with provided name and options.
|
||||
NewClient func(opt *Options) *Client
|
||||
|
||||
@@ -57,6 +62,7 @@ type ClusterOptions struct {
|
||||
|
||||
OnConnect func(ctx context.Context, cn *Conn) error
|
||||
|
||||
Protocol int
|
||||
Username string
|
||||
Password string
|
||||
|
||||
@@ -64,22 +70,24 @@ type ClusterOptions struct {
|
||||
MinRetryBackoff time.Duration
|
||||
MaxRetryBackoff time.Duration
|
||||
|
||||
DialTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
DialTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
ContextTimeoutEnabled bool
|
||||
|
||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
||||
PoolFIFO bool
|
||||
PoolFIFO bool
|
||||
PoolSize int // applies per cluster node and not for the whole cluster
|
||||
PoolTimeout time.Duration
|
||||
MinIdleConns int
|
||||
MaxIdleConns int
|
||||
MaxActiveConns int // applies per cluster node and not for the whole cluster
|
||||
ConnMaxIdleTime time.Duration
|
||||
ConnMaxLifetime time.Duration
|
||||
|
||||
// PoolSize applies per cluster node and not for the whole cluster.
|
||||
PoolSize int
|
||||
MinIdleConns int
|
||||
MaxConnAge time.Duration
|
||||
PoolTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
IdleCheckFrequency time.Duration
|
||||
TLSConfig *tls.Config
|
||||
DisableIndentity bool // Disable set-lib on connect. Default is false.
|
||||
|
||||
TLSConfig *tls.Config
|
||||
IdentitySuffix string // Add suffix to client name. Default is empty.
|
||||
}
|
||||
|
||||
func (opt *ClusterOptions) init() {
|
||||
@@ -131,13 +139,139 @@ func (opt *ClusterOptions) init() {
|
||||
}
|
||||
}
|
||||
|
||||
// ParseClusterURL parses a URL into ClusterOptions that can be used to connect to Redis.
|
||||
// The URL must be in the form:
|
||||
//
|
||||
// redis://<user>:<password>@<host>:<port>
|
||||
// or
|
||||
// rediss://<user>:<password>@<host>:<port>
|
||||
//
|
||||
// To add additional addresses, specify the query parameter, "addr" one or more times. e.g:
|
||||
//
|
||||
// redis://<user>:<password>@<host>:<port>?addr=<host2>:<port2>&addr=<host3>:<port3>
|
||||
// or
|
||||
// rediss://<user>:<password>@<host>:<port>?addr=<host2>:<port2>&addr=<host3>:<port3>
|
||||
//
|
||||
// Most Option fields can be set using query parameters, with the following restrictions:
|
||||
// - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
|
||||
// - only scalar type fields are supported (bool, int, time.Duration)
|
||||
// - for time.Duration fields, values must be a valid input for time.ParseDuration();
|
||||
// additionally a plain integer as value (i.e. without unit) is intepreted as seconds
|
||||
// - to disable a duration field, use value less than or equal to 0; to use the default
|
||||
// value, leave the value blank or remove the parameter
|
||||
// - only the last value is interpreted if a parameter is given multiple times
|
||||
// - fields "network", "addr", "username" and "password" can only be set using other
|
||||
// URL attributes (scheme, host, userinfo, resp.), query paremeters using these
|
||||
// names will be treated as unknown parameters
|
||||
// - unknown parameter names will result in an error
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// redis://user:password@localhost:6789?dial_timeout=3&read_timeout=6s&addr=localhost:6790&addr=localhost:6791
|
||||
// is equivalent to:
|
||||
// &ClusterOptions{
|
||||
// Addr: ["localhost:6789", "localhost:6790", "localhost:6791"]
|
||||
// DialTimeout: 3 * time.Second, // no time unit = seconds
|
||||
// ReadTimeout: 6 * time.Second,
|
||||
// }
|
||||
func ParseClusterURL(redisURL string) (*ClusterOptions, error) {
|
||||
o := &ClusterOptions{}
|
||||
|
||||
u, err := url.Parse(redisURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// add base URL to the array of addresses
|
||||
// more addresses may be added through the URL params
|
||||
h, p := getHostPortWithDefaults(u)
|
||||
o.Addrs = append(o.Addrs, net.JoinHostPort(h, p))
|
||||
|
||||
// setup username, password, and other configurations
|
||||
o, err = setupClusterConn(u, h, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// setupClusterConn gets the username and password from the URL and the query parameters.
|
||||
func setupClusterConn(u *url.URL, host string, o *ClusterOptions) (*ClusterOptions, error) {
|
||||
switch u.Scheme {
|
||||
case "rediss":
|
||||
o.TLSConfig = &tls.Config{ServerName: host}
|
||||
fallthrough
|
||||
case "redis":
|
||||
o.Username, o.Password = getUserPassword(u)
|
||||
default:
|
||||
return nil, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme)
|
||||
}
|
||||
|
||||
// retrieve the configuration from the query parameters
|
||||
o, err := setupClusterQueryParams(u, o)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// setupClusterQueryParams converts query parameters in u to option value in o.
|
||||
func setupClusterQueryParams(u *url.URL, o *ClusterOptions) (*ClusterOptions, error) {
|
||||
q := queryOptions{q: u.Query()}
|
||||
|
||||
o.Protocol = q.int("protocol")
|
||||
o.ClientName = q.string("client_name")
|
||||
o.MaxRedirects = q.int("max_redirects")
|
||||
o.ReadOnly = q.bool("read_only")
|
||||
o.RouteByLatency = q.bool("route_by_latency")
|
||||
o.RouteRandomly = q.bool("route_randomly")
|
||||
o.MaxRetries = q.int("max_retries")
|
||||
o.MinRetryBackoff = q.duration("min_retry_backoff")
|
||||
o.MaxRetryBackoff = q.duration("max_retry_backoff")
|
||||
o.DialTimeout = q.duration("dial_timeout")
|
||||
o.ReadTimeout = q.duration("read_timeout")
|
||||
o.WriteTimeout = q.duration("write_timeout")
|
||||
o.PoolFIFO = q.bool("pool_fifo")
|
||||
o.PoolSize = q.int("pool_size")
|
||||
o.MinIdleConns = q.int("min_idle_conns")
|
||||
o.MaxIdleConns = q.int("max_idle_conns")
|
||||
o.MaxActiveConns = q.int("max_active_conns")
|
||||
o.PoolTimeout = q.duration("pool_timeout")
|
||||
o.ConnMaxLifetime = q.duration("conn_max_lifetime")
|
||||
o.ConnMaxIdleTime = q.duration("conn_max_idle_time")
|
||||
|
||||
if q.err != nil {
|
||||
return nil, q.err
|
||||
}
|
||||
|
||||
// addr can be specified as many times as needed
|
||||
addrs := q.strings("addr")
|
||||
for _, addr := range addrs {
|
||||
h, p, err := net.SplitHostPort(addr)
|
||||
if err != nil || h == "" || p == "" {
|
||||
return nil, fmt.Errorf("redis: unable to parse addr param: %s", addr)
|
||||
}
|
||||
|
||||
o.Addrs = append(o.Addrs, net.JoinHostPort(h, p))
|
||||
}
|
||||
|
||||
// any parameters left?
|
||||
if r := q.remaining(); len(r) > 0 {
|
||||
return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", "))
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
||||
|
||||
func (opt *ClusterOptions) clientOptions() *Options {
|
||||
const disableIdleCheck = -1
|
||||
|
||||
return &Options{
|
||||
Dialer: opt.Dialer,
|
||||
OnConnect: opt.OnConnect,
|
||||
ClientName: opt.ClientName,
|
||||
Dialer: opt.Dialer,
|
||||
OnConnect: opt.OnConnect,
|
||||
|
||||
Protocol: opt.Protocol,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
|
||||
@@ -145,19 +279,22 @@ func (opt *ClusterOptions) clientOptions() *Options {
|
||||
MinRetryBackoff: opt.MinRetryBackoff,
|
||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
|
||||
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxConnAge: opt.MaxConnAge,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
IdleTimeout: opt.IdleTimeout,
|
||||
IdleCheckFrequency: disableIdleCheck,
|
||||
|
||||
TLSConfig: opt.TLSConfig,
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxIdleConns: opt.MaxIdleConns,
|
||||
MaxActiveConns: opt.MaxActiveConns,
|
||||
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||
DisableIndentity: opt.DisableIndentity,
|
||||
IdentitySuffix: opt.IdentitySuffix,
|
||||
TLSConfig: opt.TLSConfig,
|
||||
// If ClusterSlots is populated, then we probably have an artificial
|
||||
// cluster whose nodes are not in clustering mode (otherwise there isn't
|
||||
// much use for ClusterSlots config). This means we cannot execute the
|
||||
@@ -204,15 +341,26 @@ func (n *clusterNode) updateLatency() {
|
||||
const numProbe = 10
|
||||
var dur uint64
|
||||
|
||||
successes := 0
|
||||
for i := 0; i < numProbe; i++ {
|
||||
time.Sleep(time.Duration(10+rand.Intn(10)) * time.Millisecond)
|
||||
|
||||
start := time.Now()
|
||||
n.Client.Ping(context.TODO())
|
||||
dur += uint64(time.Since(start) / time.Microsecond)
|
||||
err := n.Client.Ping(context.TODO()).Err()
|
||||
if err == nil {
|
||||
dur += uint64(time.Since(start) / time.Microsecond)
|
||||
successes++
|
||||
}
|
||||
}
|
||||
|
||||
latency := float64(dur) / float64(numProbe)
|
||||
var latency float64
|
||||
if successes == 0 {
|
||||
// If none of the pings worked, set latency to some arbitrarily high value so this node gets
|
||||
// least priority.
|
||||
latency = float64((1 * time.Minute) / time.Microsecond)
|
||||
} else {
|
||||
latency = float64(dur) / float64(successes)
|
||||
}
|
||||
atomic.StoreUint32(&n.latency, uint32(latency+0.5))
|
||||
}
|
||||
|
||||
@@ -262,6 +410,7 @@ type clusterNodes struct {
|
||||
nodes map[string]*clusterNode
|
||||
activeAddrs []string
|
||||
closed bool
|
||||
onNewNode []func(rdb *Client)
|
||||
|
||||
_generation uint32 // atomic
|
||||
}
|
||||
@@ -297,6 +446,12 @@ func (c *clusterNodes) Close() error {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (c *clusterNodes) OnNewNode(fn func(rdb *Client)) {
|
||||
c.mu.Lock()
|
||||
c.onNewNode = append(c.onNewNode, fn)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *clusterNodes) Addrs() ([]string, error) {
|
||||
var addrs []string
|
||||
|
||||
@@ -374,6 +529,9 @@ func (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) {
|
||||
}
|
||||
|
||||
node = newClusterNode(c.opt, addr)
|
||||
for _, fn := range c.onNewNode {
|
||||
fn(node.Client)
|
||||
}
|
||||
|
||||
c.addrs = appendIfNotExists(c.addrs, addr)
|
||||
c.nodes[addr] = node
|
||||
@@ -683,21 +841,16 @@ func (c *clusterStateHolder) ReloadOrGet(ctx context.Context) (*clusterState, er
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type clusterClient struct {
|
||||
opt *ClusterOptions
|
||||
nodes *clusterNodes
|
||||
state *clusterStateHolder //nolint:structcheck
|
||||
cmdsInfoCache *cmdsInfoCache //nolint:structcheck
|
||||
}
|
||||
|
||||
// ClusterClient is a Redis Cluster client representing a pool of zero
|
||||
// or more underlying connections. It's safe for concurrent use by
|
||||
// multiple goroutines.
|
||||
type ClusterClient struct {
|
||||
*clusterClient
|
||||
opt *ClusterOptions
|
||||
nodes *clusterNodes
|
||||
state *clusterStateHolder
|
||||
cmdsInfoCache *cmdsInfoCache
|
||||
cmdable
|
||||
hooks
|
||||
ctx context.Context
|
||||
hooksMixin
|
||||
}
|
||||
|
||||
// NewClusterClient returns a Redis Cluster client as described in
|
||||
@@ -706,38 +859,24 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient {
|
||||
opt.init()
|
||||
|
||||
c := &ClusterClient{
|
||||
clusterClient: &clusterClient{
|
||||
opt: opt,
|
||||
nodes: newClusterNodes(opt),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
opt: opt,
|
||||
nodes: newClusterNodes(opt),
|
||||
}
|
||||
|
||||
c.state = newClusterStateHolder(c.loadState)
|
||||
c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo)
|
||||
c.cmdable = c.Process
|
||||
|
||||
if opt.IdleCheckFrequency > 0 {
|
||||
go c.reaper(opt.IdleCheckFrequency)
|
||||
}
|
||||
c.initHooks(hooks{
|
||||
dial: nil,
|
||||
process: c.process,
|
||||
pipeline: c.processPipeline,
|
||||
txPipeline: c.processTxPipeline,
|
||||
})
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *ClusterClient) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *ClusterClient) WithContext(ctx context.Context) *ClusterClient {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
clone := *c
|
||||
clone.cmdable = clone.Process
|
||||
clone.hooks.lock()
|
||||
clone.ctx = ctx
|
||||
return &clone
|
||||
}
|
||||
|
||||
// Options returns read-only Options that were used to create the client.
|
||||
func (c *ClusterClient) Options() *ClusterOptions {
|
||||
return c.opt
|
||||
@@ -757,7 +896,7 @@ func (c *ClusterClient) Close() error {
|
||||
return c.nodes.Close()
|
||||
}
|
||||
|
||||
// Do creates a Cmd from the args and processes the cmd.
|
||||
// Do create a Cmd from the args and processes the cmd.
|
||||
func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c.Process(ctx, cmd)
|
||||
@@ -765,13 +904,13 @@ func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
}
|
||||
|
||||
func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error {
|
||||
return c.hooks.process(ctx, cmd, c.process)
|
||||
err := c.processHook(ctx, cmd)
|
||||
cmd.SetErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||
cmdInfo := c.cmdInfo(cmd.Name())
|
||||
slot := c.cmdSlot(cmd)
|
||||
|
||||
slot := c.cmdSlot(ctx, cmd)
|
||||
var node *clusterNode
|
||||
var ask bool
|
||||
var lastErr error
|
||||
@@ -784,19 +923,19 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||
|
||||
if node == nil {
|
||||
var err error
|
||||
node, err = c.cmdNode(ctx, cmdInfo, slot)
|
||||
node, err = c.cmdNode(ctx, cmd.Name(), slot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ask {
|
||||
ask = false
|
||||
|
||||
pipe := node.Client.Pipeline()
|
||||
_ = pipe.Process(ctx, NewCmd(ctx, "asking"))
|
||||
_ = pipe.Process(ctx, cmd)
|
||||
_, lastErr = pipe.Exec(ctx)
|
||||
_ = pipe.Close()
|
||||
ask = false
|
||||
} else {
|
||||
lastErr = node.Client.Process(ctx, cmd)
|
||||
}
|
||||
@@ -851,6 +990,10 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *ClusterClient) OnNewNode(fn func(rdb *Client)) {
|
||||
c.nodes.OnNewNode(fn)
|
||||
}
|
||||
|
||||
// ForEachMaster concurrently calls the fn on each master node in the cluster.
|
||||
// It returns the first error if any.
|
||||
func (c *ClusterClient) ForEachMaster(
|
||||
@@ -1056,30 +1199,9 @@ func (c *ClusterClient) loadState(ctx context.Context) (*clusterState, error) {
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
// reaper closes idle connections to the cluster.
|
||||
func (c *ClusterClient) reaper(idleCheckFrequency time.Duration) {
|
||||
ticker := time.NewTicker(idleCheckFrequency)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
nodes, err := c.nodes.All()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
_, err := node.Client.connPool.(*pool.ConnPool).ReapStaleConns()
|
||||
if err != nil {
|
||||
internal.Logger.Printf(c.Context(), "ReapStaleConns failed: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ClusterClient) Pipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processPipeline,
|
||||
exec: pipelineExecer(c.processPipelineHook),
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
@@ -1090,13 +1212,9 @@ func (c *ClusterClient) Pipelined(ctx context.Context, fn func(Pipeliner) error)
|
||||
}
|
||||
|
||||
func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processPipeline(ctx, cmds, c._processPipeline)
|
||||
}
|
||||
|
||||
func (c *ClusterClient) _processPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
cmdsMap := newCmdsMap()
|
||||
err := c.mapCmdsByNode(ctx, cmdsMap, cmds)
|
||||
if err != nil {
|
||||
|
||||
if err := c.mapCmdsByNode(ctx, cmdsMap, cmds); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
@@ -1116,18 +1234,7 @@ func (c *ClusterClient) _processPipeline(ctx context.Context, cmds []Cmder) erro
|
||||
wg.Add(1)
|
||||
go func(node *clusterNode, cmds []Cmder) {
|
||||
defer wg.Done()
|
||||
|
||||
err := c._processPipelineNode(ctx, node, cmds, failedCmds)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if attempt < c.opt.MaxRedirects {
|
||||
if err := c.mapCmdsByNode(ctx, failedCmds, cmds); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
}
|
||||
} else {
|
||||
setCmdsErr(cmds, err)
|
||||
}
|
||||
c.processPipelineNode(ctx, node, cmds, failedCmds)
|
||||
}(node, cmds)
|
||||
}
|
||||
|
||||
@@ -1147,9 +1254,9 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||
return err
|
||||
}
|
||||
|
||||
if c.opt.ReadOnly && c.cmdsAreReadOnly(cmds) {
|
||||
if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {
|
||||
for _, cmd := range cmds {
|
||||
slot := c.cmdSlot(cmd)
|
||||
slot := c.cmdSlot(ctx, cmd)
|
||||
node, err := c.slotReadOnlyNode(state, slot)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1160,7 +1267,7 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||
}
|
||||
|
||||
for _, cmd := range cmds {
|
||||
slot := c.cmdSlot(cmd)
|
||||
slot := c.cmdSlot(ctx, cmd)
|
||||
node, err := state.slotMasterNode(slot)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1170,9 +1277,9 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ClusterClient) cmdsAreReadOnly(cmds []Cmder) bool {
|
||||
func (c *ClusterClient) cmdsAreReadOnly(ctx context.Context, cmds []Cmder) bool {
|
||||
for _, cmd := range cmds {
|
||||
cmdInfo := c.cmdInfo(cmd.Name())
|
||||
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||
if cmdInfo == nil || !cmdInfo.ReadOnly {
|
||||
return false
|
||||
}
|
||||
@@ -1180,22 +1287,42 @@ func (c *ClusterClient) cmdsAreReadOnly(cmds []Cmder) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *ClusterClient) _processPipelineNode(
|
||||
func (c *ClusterClient) processPipelineNode(
|
||||
ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap,
|
||||
) error {
|
||||
return node.Client.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
||||
return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmds(wr, cmds)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
) {
|
||||
_ = node.Client.withProcessPipelineHook(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
||||
cn, err := node.Client.getConn(ctx)
|
||||
if err != nil {
|
||||
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
return c.pipelineReadCmds(ctx, node, rd, cmds, failedCmds)
|
||||
})
|
||||
})
|
||||
var processErr error
|
||||
defer func() {
|
||||
node.Client.releaseConn(ctx, cn, processErr)
|
||||
}()
|
||||
processErr = c.processPipelineNodeConn(ctx, node, cn, cmds, failedCmds)
|
||||
|
||||
return processErr
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ClusterClient) processPipelineNodeConn(
|
||||
ctx context.Context, node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap,
|
||||
) error {
|
||||
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) {
|
||||
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||
}
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
return c.pipelineReadCmds(ctx, node, rd, cmds, failedCmds)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1206,7 +1333,7 @@ func (c *ClusterClient) pipelineReadCmds(
|
||||
cmds []Cmder,
|
||||
failedCmds *cmdsMap,
|
||||
) error {
|
||||
for _, cmd := range cmds {
|
||||
for i, cmd := range cmds {
|
||||
err := cmd.readReply(rd)
|
||||
cmd.SetErr(err)
|
||||
|
||||
@@ -1218,15 +1345,24 @@ func (c *ClusterClient) pipelineReadCmds(
|
||||
continue
|
||||
}
|
||||
|
||||
if c.opt.ReadOnly && isLoadingError(err) {
|
||||
if c.opt.ReadOnly {
|
||||
node.MarkAsFailing()
|
||||
}
|
||||
|
||||
if !isRedisError(err) {
|
||||
if shouldRetry(err, true) {
|
||||
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||
}
|
||||
setCmdsErr(cmds[i+1:], err)
|
||||
return err
|
||||
}
|
||||
if isRedisError(err) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmds[0].Err(); err != nil && shouldRetry(err, true) {
|
||||
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1260,8 +1396,10 @@ func (c *ClusterClient) checkMovedErr(
|
||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
||||
func (c *ClusterClient) TxPipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processTxPipeline,
|
||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
return c.processTxPipelineHook(ctx, cmds)
|
||||
},
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
@@ -1272,10 +1410,6 @@ func (c *ClusterClient) TxPipelined(ctx context.Context, fn func(Pipeliner) erro
|
||||
}
|
||||
|
||||
func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processTxPipeline(ctx, cmds, c._processTxPipeline)
|
||||
}
|
||||
|
||||
func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
// Trim multi .. exec.
|
||||
cmds = cmds[1 : len(cmds)-1]
|
||||
|
||||
@@ -1285,7 +1419,7 @@ func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) er
|
||||
return err
|
||||
}
|
||||
|
||||
cmdsMap := c.mapCmdsBySlot(cmds)
|
||||
cmdsMap := c.mapCmdsBySlot(ctx, cmds)
|
||||
for slot, cmds := range cmdsMap {
|
||||
node, err := state.slotMasterNode(slot)
|
||||
if err != nil {
|
||||
@@ -1309,19 +1443,7 @@ func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) er
|
||||
wg.Add(1)
|
||||
go func(node *clusterNode, cmds []Cmder) {
|
||||
defer wg.Done()
|
||||
|
||||
err := c._processTxPipelineNode(ctx, node, cmds, failedCmds)
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if attempt < c.opt.MaxRedirects {
|
||||
if err := c.mapCmdsByNode(ctx, failedCmds, cmds); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
}
|
||||
} else {
|
||||
setCmdsErr(cmds, err)
|
||||
}
|
||||
c.processTxPipelineNode(ctx, node, cmds, failedCmds)
|
||||
}(node, cmds)
|
||||
}
|
||||
|
||||
@@ -1336,44 +1458,69 @@ func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) er
|
||||
return cmdsFirstErr(cmds)
|
||||
}
|
||||
|
||||
func (c *ClusterClient) mapCmdsBySlot(cmds []Cmder) map[int][]Cmder {
|
||||
func (c *ClusterClient) mapCmdsBySlot(ctx context.Context, cmds []Cmder) map[int][]Cmder {
|
||||
cmdsMap := make(map[int][]Cmder)
|
||||
for _, cmd := range cmds {
|
||||
slot := c.cmdSlot(cmd)
|
||||
slot := c.cmdSlot(ctx, cmd)
|
||||
cmdsMap[slot] = append(cmdsMap[slot], cmd)
|
||||
}
|
||||
return cmdsMap
|
||||
}
|
||||
|
||||
func (c *ClusterClient) _processTxPipelineNode(
|
||||
func (c *ClusterClient) processTxPipelineNode(
|
||||
ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap,
|
||||
) {
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
_ = node.Client.withProcessPipelineHook(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
||||
cn, err := node.Client.getConn(ctx)
|
||||
if err != nil {
|
||||
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
|
||||
var processErr error
|
||||
defer func() {
|
||||
node.Client.releaseConn(ctx, cn, processErr)
|
||||
}()
|
||||
processErr = c.processTxPipelineNodeConn(ctx, node, cn, cmds, failedCmds)
|
||||
|
||||
return processErr
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ClusterClient) processTxPipelineNodeConn(
|
||||
ctx context.Context, node *clusterNode, cn *pool.Conn, cmds []Cmder, failedCmds *cmdsMap,
|
||||
) error {
|
||||
return node.Client.hooks.processTxPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
||||
return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmds(wr, cmds)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
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) {
|
||||
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||
}
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
statusCmd := cmds[0].(*StatusCmd)
|
||||
// Trim multi and exec.
|
||||
trimmedCmds := cmds[1 : len(cmds)-1]
|
||||
|
||||
if err := c.txPipelineReadQueued(
|
||||
ctx, rd, statusCmd, trimmedCmds, failedCmds,
|
||||
); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
|
||||
moved, ask, addr := isMovedError(err)
|
||||
if moved || ask {
|
||||
return c.cmdsMoved(ctx, trimmedCmds, moved, ask, addr, failedCmds)
|
||||
}
|
||||
|
||||
return cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
statusCmd := cmds[0].(*StatusCmd)
|
||||
// Trim multi and exec.
|
||||
cmds = cmds[1 : len(cmds)-1]
|
||||
return err
|
||||
}
|
||||
|
||||
err := c.txPipelineReadQueued(ctx, rd, statusCmd, cmds, failedCmds)
|
||||
if err != nil {
|
||||
moved, ask, addr := isMovedError(err)
|
||||
if moved || ask {
|
||||
return c.cmdsMoved(ctx, cmds, moved, ask, addr, failedCmds)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return pipelineReadCmds(rd, cmds)
|
||||
})
|
||||
})
|
||||
return pipelineReadCmds(rd, trimmedCmds)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1406,12 +1553,7 @@ func (c *ClusterClient) txPipelineReadQueued(
|
||||
return err
|
||||
}
|
||||
|
||||
switch line[0] {
|
||||
case proto.ErrorReply:
|
||||
return proto.ParseErrorReply(line)
|
||||
case proto.ArrayReply:
|
||||
// ok
|
||||
default:
|
||||
if line[0] != proto.RespArray {
|
||||
return fmt.Errorf("redis: expected '*', but got line %q", line)
|
||||
}
|
||||
|
||||
@@ -1568,6 +1710,15 @@ func (c *ClusterClient) PSubscribe(ctx context.Context, channels ...string) *Pub
|
||||
return pubsub
|
||||
}
|
||||
|
||||
// SSubscribe Subscribes the client to the specified shard channels.
|
||||
func (c *ClusterClient) SSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
pubsub := c.pubSub()
|
||||
if len(channels) > 0 {
|
||||
_ = pubsub.SSubscribe(ctx, channels...)
|
||||
}
|
||||
return pubsub
|
||||
}
|
||||
|
||||
func (c *ClusterClient) retryBackoff(attempt int) time.Duration {
|
||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
|
||||
}
|
||||
@@ -1614,27 +1765,27 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
func (c *ClusterClient) cmdInfo(name string) *CommandInfo {
|
||||
cmdsInfo, err := c.cmdsInfoCache.Get(c.ctx)
|
||||
func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo {
|
||||
cmdsInfo, err := c.cmdsInfoCache.Get(ctx)
|
||||
if err != nil {
|
||||
internal.Logger.Printf(context.TODO(), "getting command info: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
info := cmdsInfo[name]
|
||||
if info == nil {
|
||||
internal.Logger.Printf(c.Context(), "info for cmd=%s not found", name)
|
||||
internal.Logger.Printf(context.TODO(), "info for cmd=%s not found", name)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (c *ClusterClient) cmdSlot(cmd Cmder) int {
|
||||
func (c *ClusterClient) cmdSlot(ctx context.Context, cmd Cmder) int {
|
||||
args := cmd.Args()
|
||||
if args[0] == "cluster" && args[1] == "getkeysinslot" {
|
||||
return args[2].(int)
|
||||
}
|
||||
|
||||
cmdInfo := c.cmdInfo(cmd.Name())
|
||||
return cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo))
|
||||
return cmdSlot(cmd, cmdFirstKeyPos(cmd))
|
||||
}
|
||||
|
||||
func cmdSlot(cmd Cmder, pos int) int {
|
||||
@@ -1647,7 +1798,7 @@ func cmdSlot(cmd Cmder, pos int) int {
|
||||
|
||||
func (c *ClusterClient) cmdNode(
|
||||
ctx context.Context,
|
||||
cmdInfo *CommandInfo,
|
||||
cmdName string,
|
||||
slot int,
|
||||
) (*clusterNode, error) {
|
||||
state, err := c.state.Get(ctx)
|
||||
@@ -1655,13 +1806,16 @@ func (c *ClusterClient) cmdNode(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.opt.ReadOnly && cmdInfo != nil && cmdInfo.ReadOnly {
|
||||
return c.slotReadOnlyNode(state, slot)
|
||||
if c.opt.ReadOnly {
|
||||
cmdInfo := c.cmdInfo(ctx, cmdName)
|
||||
if cmdInfo != nil && cmdInfo.ReadOnly {
|
||||
return c.slotReadOnlyNode(state, slot)
|
||||
}
|
||||
}
|
||||
return state.slotMasterNode(slot)
|
||||
}
|
||||
|
||||
func (c *clusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) {
|
||||
func (c *ClusterClient) slotReadOnlyNode(state *clusterState, slot int) (*clusterNode, error) {
|
||||
if c.opt.RouteByLatency {
|
||||
return state.slotClosestNode(slot)
|
||||
}
|
||||
@@ -1708,6 +1862,13 @@ func (c *ClusterClient) MasterForKey(ctx context.Context, key string) (*Client,
|
||||
return node.Client, err
|
||||
}
|
||||
|
||||
func (c *ClusterClient) context(ctx context.Context) context.Context {
|
||||
if c.opt.ContextTimeoutEnabled {
|
||||
return ctx
|
||||
}
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
func appendUniqueNode(nodes []*clusterNode, node *clusterNode) []*clusterNode {
|
||||
for _, n := range nodes {
|
||||
if n == node {
|
||||
Generated
Vendored
+6
-6
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "dbsize")
|
||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
var size int64
|
||||
err := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error {
|
||||
n, err := master.DBSize(ctx).Result()
|
||||
@@ -30,8 +30,8 @@ func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd {
|
||||
|
||||
func (c *ClusterClient) ScriptLoad(ctx context.Context, script string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "script", "load", script)
|
||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
mu := &sync.Mutex{}
|
||||
_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
var mu sync.Mutex
|
||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
||||
val, err := shard.ScriptLoad(ctx, script).Result()
|
||||
if err != nil {
|
||||
@@ -56,7 +56,7 @@ func (c *ClusterClient) ScriptLoad(ctx context.Context, script string) *StringCm
|
||||
|
||||
func (c *ClusterClient) ScriptFlush(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "script", "flush")
|
||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
||||
return shard.ScriptFlush(ctx).Err()
|
||||
})
|
||||
@@ -82,8 +82,8 @@ func (c *ClusterClient) ScriptExists(ctx context.Context, hashes ...string) *Boo
|
||||
result[i] = true
|
||||
}
|
||||
|
||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
mu := &sync.Mutex{}
|
||||
_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||
var mu sync.Mutex
|
||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
||||
val, err := shard.ScriptExists(ctx, hashes...).Result()
|
||||
if err != nil {
|
||||
Generated
Vendored
+2
-2
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "redis",
|
||||
"version": "8.11.5",
|
||||
"version": "9.4.0",
|
||||
"main": "index.js",
|
||||
"repository": "git@github.com:go-redis/redis.git",
|
||||
"repository": "git@github.com:redis/go-redis.git",
|
||||
"author": "Vladimir Mihailenco <vladimir.webdev@gmail.com>",
|
||||
"license": "BSD-2-clause"
|
||||
}
|
||||
Generated
Vendored
+24
-50
@@ -2,9 +2,7 @@ package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type pipelineExecer func(context.Context, []Cmder) error
|
||||
@@ -13,7 +11,7 @@ type pipelineExecer func(context.Context, []Cmder) error
|
||||
//
|
||||
// Pipelining is a technique to extremely speed up processing by packing
|
||||
// operations to batches, send them at once to Redis and read a replies in a
|
||||
// singe step.
|
||||
// single step.
|
||||
// See https://redis.io/topics/pipelining
|
||||
//
|
||||
// Pay attention, that Pipeline is not a transaction, so you can get unexpected
|
||||
@@ -24,29 +22,35 @@ type pipelineExecer func(context.Context, []Cmder) error
|
||||
// depends of your batch size and/or use TxPipeline.
|
||||
type Pipeliner interface {
|
||||
StatefulCmdable
|
||||
|
||||
// Len is to obtain the number of commands in the pipeline that have not yet been executed.
|
||||
Len() int
|
||||
|
||||
// Do is an API for executing any command.
|
||||
// If a certain Redis command is not yet supported, you can use Do to execute it.
|
||||
Do(ctx context.Context, args ...interface{}) *Cmd
|
||||
|
||||
// Process is to put the commands to be executed into the pipeline buffer.
|
||||
Process(ctx context.Context, cmd Cmder) error
|
||||
Close() error
|
||||
Discard() error
|
||||
|
||||
// Discard is to discard all commands in the cache that have not yet been executed.
|
||||
Discard()
|
||||
|
||||
// Exec is to send all the commands buffered in the pipeline to the redis-server.
|
||||
Exec(ctx context.Context) ([]Cmder, error)
|
||||
}
|
||||
|
||||
var _ Pipeliner = (*Pipeline)(nil)
|
||||
|
||||
// Pipeline implements pipelining as described in
|
||||
// http://redis.io/topics/pipelining. It's safe for concurrent use
|
||||
// by multiple goroutines.
|
||||
// http://redis.io/topics/pipelining.
|
||||
// Please note: it is not safe for concurrent use by multiple goroutines.
|
||||
type Pipeline struct {
|
||||
cmdable
|
||||
statefulCmdable
|
||||
|
||||
ctx context.Context
|
||||
exec pipelineExecer
|
||||
|
||||
mu sync.Mutex
|
||||
cmds []Cmder
|
||||
closed bool
|
||||
cmds []Cmder
|
||||
}
|
||||
|
||||
func (c *Pipeline) init() {
|
||||
@@ -56,50 +60,29 @@ func (c *Pipeline) init() {
|
||||
|
||||
// Len returns the number of queued commands.
|
||||
func (c *Pipeline) Len() int {
|
||||
c.mu.Lock()
|
||||
ln := len(c.cmds)
|
||||
c.mu.Unlock()
|
||||
return ln
|
||||
return len(c.cmds)
|
||||
}
|
||||
|
||||
// Do queues the custom command for later execution.
|
||||
func (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
cmd := NewCmd(ctx, args...)
|
||||
if len(args) == 0 {
|
||||
cmd.SetErr(errors.New("redis: please enter the command to be executed"))
|
||||
return cmd
|
||||
}
|
||||
_ = c.Process(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Process queues the cmd for later execution.
|
||||
func (c *Pipeline) Process(ctx context.Context, cmd Cmder) error {
|
||||
c.mu.Lock()
|
||||
c.cmds = append(c.cmds, cmd)
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the pipeline, releasing any open resources.
|
||||
func (c *Pipeline) Close() error {
|
||||
c.mu.Lock()
|
||||
_ = c.discard()
|
||||
c.closed = true
|
||||
c.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Discard resets the pipeline and discards queued commands.
|
||||
func (c *Pipeline) Discard() error {
|
||||
c.mu.Lock()
|
||||
err := c.discard()
|
||||
c.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Pipeline) discard() error {
|
||||
if c.closed {
|
||||
return pool.ErrClosed
|
||||
}
|
||||
func (c *Pipeline) Discard() {
|
||||
c.cmds = c.cmds[:0]
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec executes all previously queued commands using one
|
||||
@@ -108,13 +91,6 @@ func (c *Pipeline) discard() error {
|
||||
// Exec always returns list of commands and error of the first failed
|
||||
// command if any.
|
||||
func (c *Pipeline) Exec(ctx context.Context) ([]Cmder, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return nil, pool.ErrClosed
|
||||
}
|
||||
|
||||
if len(c.cmds) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -129,9 +105,7 @@ func (c *Pipeline) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]C
|
||||
if err := fn(c); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmds, err := c.Exec(ctx)
|
||||
_ = c.Close()
|
||||
return cmds, err
|
||||
return c.Exec(ctx)
|
||||
}
|
||||
|
||||
func (c *Pipeline) Pipeline() Pipeliner {
|
||||
+1429
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+80
-19
@@ -7,9 +7,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal"
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/go-redis/redis/v8/internal/proto"
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
// PubSub implements Pub/Sub commands as described in
|
||||
@@ -24,10 +24,11 @@ type PubSub struct {
|
||||
newConn func(ctx context.Context, channels []string) (*pool.Conn, error)
|
||||
closeConn func(*pool.Conn) error
|
||||
|
||||
mu sync.Mutex
|
||||
cn *pool.Conn
|
||||
channels map[string]struct{}
|
||||
patterns map[string]struct{}
|
||||
mu sync.Mutex
|
||||
cn *pool.Conn
|
||||
channels map[string]struct{}
|
||||
patterns map[string]struct{}
|
||||
schannels map[string]struct{}
|
||||
|
||||
closed bool
|
||||
exit chan struct{}
|
||||
@@ -46,6 +47,7 @@ func (c *PubSub) init() {
|
||||
func (c *PubSub) String() string {
|
||||
channels := mapKeys(c.channels)
|
||||
channels = append(channels, mapKeys(c.patterns)...)
|
||||
channels = append(channels, mapKeys(c.schannels)...)
|
||||
return fmt.Sprintf("PubSub(%s)", strings.Join(channels, ", "))
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ func (c *PubSub) conn(ctx context.Context, newChannels []string) (*pool.Conn, er
|
||||
}
|
||||
|
||||
func (c *PubSub) writeCmd(ctx context.Context, cn *pool.Conn, cmd Cmder) error {
|
||||
return cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return cn.WithWriter(context.Background(), c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmd(wr, cmd)
|
||||
})
|
||||
}
|
||||
@@ -101,6 +103,13 @@ func (c *PubSub) resubscribe(ctx context.Context, cn *pool.Conn) error {
|
||||
}
|
||||
}
|
||||
|
||||
if len(c.schannels) > 0 {
|
||||
err := c._subscribe(ctx, cn, "ssubscribe", mapKeys(c.schannels))
|
||||
if err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
@@ -208,15 +217,38 @@ func (c *PubSub) PSubscribe(ctx context.Context, patterns ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SSubscribe Subscribes the client to the specified shard channels.
|
||||
func (c *PubSub) SSubscribe(ctx context.Context, channels ...string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
err := c.subscribe(ctx, "ssubscribe", channels...)
|
||||
if c.schannels == nil {
|
||||
c.schannels = make(map[string]struct{})
|
||||
}
|
||||
for _, s := range channels {
|
||||
c.schannels[s] = struct{}{}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Unsubscribe the client from the given channels, or from all of
|
||||
// them if none is given.
|
||||
func (c *PubSub) Unsubscribe(ctx context.Context, channels ...string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for _, channel := range channels {
|
||||
delete(c.channels, channel)
|
||||
if len(channels) > 0 {
|
||||
for _, channel := range channels {
|
||||
delete(c.channels, channel)
|
||||
}
|
||||
} else {
|
||||
// Unsubscribe from all channels.
|
||||
for channel := range c.channels {
|
||||
delete(c.channels, channel)
|
||||
}
|
||||
}
|
||||
|
||||
err := c.subscribe(ctx, "unsubscribe", channels...)
|
||||
return err
|
||||
}
|
||||
@@ -227,13 +259,42 @@ func (c *PubSub) PUnsubscribe(ctx context.Context, patterns ...string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for _, pattern := range patterns {
|
||||
delete(c.patterns, pattern)
|
||||
if len(patterns) > 0 {
|
||||
for _, pattern := range patterns {
|
||||
delete(c.patterns, pattern)
|
||||
}
|
||||
} else {
|
||||
// Unsubscribe from all patterns.
|
||||
for pattern := range c.patterns {
|
||||
delete(c.patterns, pattern)
|
||||
}
|
||||
}
|
||||
|
||||
err := c.subscribe(ctx, "punsubscribe", patterns...)
|
||||
return err
|
||||
}
|
||||
|
||||
// SUnsubscribe unsubscribes the client from the given shard channels,
|
||||
// or from all of them if none is given.
|
||||
func (c *PubSub) SUnsubscribe(ctx context.Context, channels ...string) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if len(channels) > 0 {
|
||||
for _, channel := range channels {
|
||||
delete(c.schannels, channel)
|
||||
}
|
||||
} else {
|
||||
// Unsubscribe from all channels.
|
||||
for channel := range c.schannels {
|
||||
delete(c.schannels, channel)
|
||||
}
|
||||
}
|
||||
|
||||
err := c.subscribe(ctx, "sunsubscribe", channels...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *PubSub) subscribe(ctx context.Context, redisCmd string, channels ...string) error {
|
||||
cn, err := c.conn(ctx, channels)
|
||||
if err != nil {
|
||||
@@ -311,7 +372,7 @@ func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
|
||||
}, nil
|
||||
case []interface{}:
|
||||
switch kind := reply[0].(string); kind {
|
||||
case "subscribe", "unsubscribe", "psubscribe", "punsubscribe":
|
||||
case "subscribe", "unsubscribe", "psubscribe", "punsubscribe", "ssubscribe", "sunsubscribe":
|
||||
// Can be nil in case of "unsubscribe".
|
||||
channel, _ := reply[1].(string)
|
||||
return &Subscription{
|
||||
@@ -319,7 +380,7 @@ func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
|
||||
Channel: channel,
|
||||
Count: int(reply[2].(int64)),
|
||||
}, nil
|
||||
case "message":
|
||||
case "message", "smessage":
|
||||
switch payload := reply[2].(type) {
|
||||
case string:
|
||||
return &Message{
|
||||
@@ -371,7 +432,7 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = cn.WithReader(ctx, timeout, func(rd *proto.Reader) error {
|
||||
err = cn.WithReader(context.Background(), timeout, func(rd *proto.Reader) error {
|
||||
return c.cmd.readReply(rd)
|
||||
})
|
||||
|
||||
@@ -426,11 +487,11 @@ func (c *PubSub) getContext() context.Context {
|
||||
|
||||
// Channel returns a Go channel for concurrently receiving messages.
|
||||
// The channel is closed together with the PubSub. If the Go channel
|
||||
// is blocked full for 30 seconds the message is dropped.
|
||||
// is blocked full for 1 minute the message is dropped.
|
||||
// Receive* APIs can not be used after channel is created.
|
||||
//
|
||||
// go-redis periodically sends ping messages to test connection health
|
||||
// and re-subscribes if ping can not not received for 30 seconds.
|
||||
// and re-subscribes if ping can not not received for 1 minute.
|
||||
func (c *PubSub) Channel(opts ...ChannelOption) <-chan *Message {
|
||||
c.chOnce.Do(func() {
|
||||
c.msgCh = newChannel(c, opts...)
|
||||
@@ -456,9 +517,9 @@ func (c *PubSub) ChannelSize(size int) <-chan *Message {
|
||||
// reconnections.
|
||||
//
|
||||
// ChannelWithSubscriptions can not be used together with Channel or ChannelSize.
|
||||
func (c *PubSub) ChannelWithSubscriptions(_ context.Context, size int) <-chan interface{} {
|
||||
func (c *PubSub) ChannelWithSubscriptions(opts ...ChannelOption) <-chan interface{} {
|
||||
c.chOnce.Do(func() {
|
||||
c.allCh = newChannel(c, WithChannelSize(size))
|
||||
c.allCh = newChannel(c, opts...)
|
||||
c.allCh.initAllChan()
|
||||
})
|
||||
if c.allCh == nil {
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
type PubSubCmdable interface {
|
||||
Publish(ctx context.Context, channel string, message interface{}) *IntCmd
|
||||
SPublish(ctx context.Context, channel string, message interface{}) *IntCmd
|
||||
PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd
|
||||
PubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd
|
||||
PubSubNumPat(ctx context.Context) *IntCmd
|
||||
PubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd
|
||||
PubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd
|
||||
}
|
||||
|
||||
// Publish posts the message to the channel.
|
||||
func (c cmdable) Publish(ctx context.Context, channel string, message interface{}) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "publish", channel, message)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SPublish(ctx context.Context, channel string, message interface{}) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "spublish", channel, message)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PubSubChannels(ctx context.Context, pattern string) *StringSliceCmd {
|
||||
args := []interface{}{"pubsub", "channels"}
|
||||
if pattern != "*" {
|
||||
args = append(args, pattern)
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PubSubNumSub(ctx context.Context, channels ...string) *MapStringIntCmd {
|
||||
args := make([]interface{}, 2+len(channels))
|
||||
args[0] = "pubsub"
|
||||
args[1] = "numsub"
|
||||
for i, channel := range channels {
|
||||
args[2+i] = channel
|
||||
}
|
||||
cmd := NewMapStringIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PubSubShardChannels(ctx context.Context, pattern string) *StringSliceCmd {
|
||||
args := []interface{}{"pubsub", "shardchannels"}
|
||||
if pattern != "*" {
|
||||
args = append(args, pattern)
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PubSubShardNumSub(ctx context.Context, channels ...string) *MapStringIntCmd {
|
||||
args := make([]interface{}, 2+len(channels))
|
||||
args[0] = "pubsub"
|
||||
args[1] = "shardnumsub"
|
||||
for i, channel := range channels {
|
||||
args[2+i] = channel
|
||||
}
|
||||
cmd := NewMapStringIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) PubSubNumPat(ctx context.Context) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "pubsub", "numpat")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+850
@@ -0,0 +1,850 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
"github.com/redis/go-redis/v9/internal/hscan"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
// Scanner internal/hscan.Scanner exposed interface.
|
||||
type Scanner = hscan.Scanner
|
||||
|
||||
// Nil reply returned by Redis when key does not exist.
|
||||
const Nil = proto.Nil
|
||||
|
||||
// SetLogger set custom log
|
||||
func SetLogger(logger internal.Logging) {
|
||||
internal.Logger = logger
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type Hook interface {
|
||||
DialHook(next DialHook) DialHook
|
||||
ProcessHook(next ProcessHook) ProcessHook
|
||||
ProcessPipelineHook(next ProcessPipelineHook) ProcessPipelineHook
|
||||
}
|
||||
|
||||
type (
|
||||
DialHook func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
ProcessHook func(ctx context.Context, cmd Cmder) error
|
||||
ProcessPipelineHook func(ctx context.Context, cmds []Cmder) error
|
||||
)
|
||||
|
||||
type hooksMixin struct {
|
||||
hooksMu *sync.Mutex
|
||||
|
||||
slice []Hook
|
||||
initial hooks
|
||||
current hooks
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) initHooks(hooks hooks) {
|
||||
hs.hooksMu = new(sync.Mutex)
|
||||
hs.initial = hooks
|
||||
hs.chain()
|
||||
}
|
||||
|
||||
type hooks struct {
|
||||
dial DialHook
|
||||
process ProcessHook
|
||||
pipeline ProcessPipelineHook
|
||||
txPipeline ProcessPipelineHook
|
||||
}
|
||||
|
||||
func (h *hooks) setDefaults() {
|
||||
if h.dial == nil {
|
||||
h.dial = func(ctx context.Context, network, addr string) (net.Conn, error) { return nil, nil }
|
||||
}
|
||||
if h.process == nil {
|
||||
h.process = func(ctx context.Context, cmd Cmder) error { return nil }
|
||||
}
|
||||
if h.pipeline == nil {
|
||||
h.pipeline = func(ctx context.Context, cmds []Cmder) error { return nil }
|
||||
}
|
||||
if h.txPipeline == nil {
|
||||
h.txPipeline = func(ctx context.Context, cmds []Cmder) error { return nil }
|
||||
}
|
||||
}
|
||||
|
||||
// AddHook is to add a hook to the queue.
|
||||
// Hook is a function executed during network connection, command execution, and pipeline,
|
||||
// it is a first-in-first-out stack queue (FIFO).
|
||||
// You need to execute the next hook in each hook, unless you want to terminate the execution of the command.
|
||||
// For example, you added hook-1, hook-2:
|
||||
//
|
||||
// client.AddHook(hook-1, hook-2)
|
||||
//
|
||||
// hook-1:
|
||||
//
|
||||
// func (Hook1) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
// return func(ctx context.Context, cmd Cmder) error {
|
||||
// print("hook-1 start")
|
||||
// next(ctx, cmd)
|
||||
// print("hook-1 end")
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// hook-2:
|
||||
//
|
||||
// func (Hook2) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
|
||||
// return func(ctx context.Context, cmd redis.Cmder) error {
|
||||
// print("hook-2 start")
|
||||
// next(ctx, cmd)
|
||||
// print("hook-2 end")
|
||||
// return nil
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The execution sequence is:
|
||||
//
|
||||
// hook-1 start -> hook-2 start -> exec redis cmd -> hook-2 end -> hook-1 end
|
||||
//
|
||||
// Please note: "next(ctx, cmd)" is very important, it will call the next hook,
|
||||
// if "next(ctx, cmd)" is not executed, the redis command will not be executed.
|
||||
func (hs *hooksMixin) AddHook(hook Hook) {
|
||||
hs.slice = append(hs.slice, hook)
|
||||
hs.chain()
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) chain() {
|
||||
hs.initial.setDefaults()
|
||||
|
||||
hs.hooksMu.Lock()
|
||||
defer hs.hooksMu.Unlock()
|
||||
|
||||
hs.current.dial = hs.initial.dial
|
||||
hs.current.process = hs.initial.process
|
||||
hs.current.pipeline = hs.initial.pipeline
|
||||
hs.current.txPipeline = hs.initial.txPipeline
|
||||
|
||||
for i := len(hs.slice) - 1; i >= 0; i-- {
|
||||
if wrapped := hs.slice[i].DialHook(hs.current.dial); wrapped != nil {
|
||||
hs.current.dial = wrapped
|
||||
}
|
||||
if wrapped := hs.slice[i].ProcessHook(hs.current.process); wrapped != nil {
|
||||
hs.current.process = wrapped
|
||||
}
|
||||
if wrapped := hs.slice[i].ProcessPipelineHook(hs.current.pipeline); wrapped != nil {
|
||||
hs.current.pipeline = wrapped
|
||||
}
|
||||
if wrapped := hs.slice[i].ProcessPipelineHook(hs.current.txPipeline); wrapped != nil {
|
||||
hs.current.txPipeline = wrapped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) clone() hooksMixin {
|
||||
hs.hooksMu.Lock()
|
||||
defer hs.hooksMu.Unlock()
|
||||
|
||||
clone := *hs
|
||||
l := len(clone.slice)
|
||||
clone.slice = clone.slice[:l:l]
|
||||
clone.hooksMu = new(sync.Mutex)
|
||||
return clone
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) withProcessHook(ctx context.Context, cmd Cmder, hook ProcessHook) error {
|
||||
for i := len(hs.slice) - 1; i >= 0; i-- {
|
||||
if wrapped := hs.slice[i].ProcessHook(hook); wrapped != nil {
|
||||
hook = wrapped
|
||||
}
|
||||
}
|
||||
return hook(ctx, cmd)
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) withProcessPipelineHook(
|
||||
ctx context.Context, cmds []Cmder, hook ProcessPipelineHook,
|
||||
) error {
|
||||
for i := len(hs.slice) - 1; i >= 0; i-- {
|
||||
if wrapped := hs.slice[i].ProcessPipelineHook(hook); wrapped != nil {
|
||||
hook = wrapped
|
||||
}
|
||||
}
|
||||
return hook(ctx, cmds)
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) dialHook(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
hs.hooksMu.Lock()
|
||||
defer hs.hooksMu.Unlock()
|
||||
return hs.current.dial(ctx, network, addr)
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) processHook(ctx context.Context, cmd Cmder) error {
|
||||
return hs.current.process(ctx, cmd)
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) processPipelineHook(ctx context.Context, cmds []Cmder) error {
|
||||
return hs.current.pipeline(ctx, cmds)
|
||||
}
|
||||
|
||||
func (hs *hooksMixin) processTxPipelineHook(ctx context.Context, cmds []Cmder) error {
|
||||
return hs.current.txPipeline(ctx, cmds)
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type baseClient struct {
|
||||
opt *Options
|
||||
connPool pool.Pooler
|
||||
|
||||
onClose func() error // hook called when client is closed
|
||||
}
|
||||
|
||||
func (c *baseClient) clone() *baseClient {
|
||||
clone := *c
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (c *baseClient) withTimeout(timeout time.Duration) *baseClient {
|
||||
opt := c.opt.clone()
|
||||
opt.ReadTimeout = timeout
|
||||
opt.WriteTimeout = timeout
|
||||
|
||||
clone := c.clone()
|
||||
clone.opt = opt
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
func (c *baseClient) String() string {
|
||||
return fmt.Sprintf("Redis<%s db:%d>", c.getAddr(), c.opt.DB)
|
||||
}
|
||||
|
||||
func (c *baseClient) newConn(ctx context.Context) (*pool.Conn, error) {
|
||||
cn, err := c.connPool.NewConn(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = c.initConn(ctx, cn)
|
||||
if err != nil {
|
||||
_ = c.connPool.CloseConn(cn)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) getConn(ctx context.Context) (*pool.Conn, error) {
|
||||
if c.opt.Limiter != nil {
|
||||
err := c.opt.Limiter.Allow()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
cn, err := c._getConn(ctx)
|
||||
if err != nil {
|
||||
if c.opt.Limiter != nil {
|
||||
c.opt.Limiter.ReportResult(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) {
|
||||
cn, err := c.connPool.Get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cn.Inited {
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
if err := c.initConn(ctx, cn); err != nil {
|
||||
c.connPool.Remove(ctx, cn, err)
|
||||
if err := errors.Unwrap(err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cn, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
|
||||
if cn.Inited {
|
||||
return nil
|
||||
}
|
||||
cn.Inited = true
|
||||
|
||||
username, password := c.opt.Username, c.opt.Password
|
||||
if c.opt.CredentialsProvider != nil {
|
||||
username, password = c.opt.CredentialsProvider()
|
||||
}
|
||||
|
||||
connPool := pool.NewSingleConnPool(c.connPool, cn)
|
||||
conn := newConn(c.opt, connPool)
|
||||
|
||||
var auth bool
|
||||
protocol := c.opt.Protocol
|
||||
// By default, use RESP3 in current version.
|
||||
if protocol < 2 {
|
||||
protocol = 3
|
||||
}
|
||||
|
||||
// for redis-server versions that do not support the HELLO command,
|
||||
// RESP2 will continue to be used.
|
||||
if err := conn.Hello(ctx, protocol, username, password, "").Err(); err == nil {
|
||||
auth = true
|
||||
} else if !isRedisError(err) {
|
||||
// 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
|
||||
// the server does not support the HELLO command.
|
||||
// The server may be a redis-server that does not support the HELLO command,
|
||||
// or it could be DragonflyDB or a third-party redis-proxy. They all respond
|
||||
// with different error string results for unsupported commands, making it
|
||||
// difficult to rely on error strings to determine all results.
|
||||
return err
|
||||
}
|
||||
if !c.opt.DisableIndentity {
|
||||
libName := ""
|
||||
libVer := Version()
|
||||
if c.opt.IdentitySuffix != "" {
|
||||
libName = c.opt.IdentitySuffix
|
||||
}
|
||||
libInfo := LibraryInfo{LibName: &libName}
|
||||
conn.ClientSetInfo(ctx, libInfo)
|
||||
libInfo = LibraryInfo{LibVer: &libVer}
|
||||
conn.ClientSetInfo(ctx, libInfo)
|
||||
}
|
||||
_, err := conn.Pipelined(ctx, func(pipe Pipeliner) error {
|
||||
if !auth && password != "" {
|
||||
if username != "" {
|
||||
pipe.AuthACL(ctx, username, password)
|
||||
} else {
|
||||
pipe.Auth(ctx, password)
|
||||
}
|
||||
}
|
||||
|
||||
if c.opt.DB > 0 {
|
||||
pipe.Select(ctx, c.opt.DB)
|
||||
}
|
||||
|
||||
if c.opt.readOnly {
|
||||
pipe.ReadOnly(ctx)
|
||||
}
|
||||
|
||||
if c.opt.ClientName != "" {
|
||||
pipe.ClientSetName(ctx, c.opt.ClientName)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if c.opt.OnConnect != nil {
|
||||
return c.opt.OnConnect(ctx, conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) {
|
||||
if c.opt.Limiter != nil {
|
||||
c.opt.Limiter.ReportResult(err)
|
||||
}
|
||||
|
||||
if isBadConn(err, false, c.opt.Addr) {
|
||||
c.connPool.Remove(ctx, cn, err)
|
||||
} else {
|
||||
c.connPool.Put(ctx, cn)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *baseClient) withConn(
|
||||
ctx context.Context, fn func(context.Context, *pool.Conn) error,
|
||||
) error {
|
||||
cn, err := c.getConn(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fnErr error
|
||||
defer func() {
|
||||
c.releaseConn(ctx, cn, fnErr)
|
||||
}()
|
||||
|
||||
fnErr = fn(ctx, cn)
|
||||
|
||||
return fnErr
|
||||
}
|
||||
|
||||
func (c *baseClient) dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return c.opt.Dialer(ctx, network, addr)
|
||||
}
|
||||
|
||||
func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
|
||||
attempt := attempt
|
||||
|
||||
retry, err := c._process(ctx, cmd, attempt)
|
||||
if err == nil || !retry {
|
||||
return err
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) {
|
||||
if attempt > 0 {
|
||||
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
retryTimeout := uint32(0)
|
||||
if err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
||||
if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmd(wr, cmd)
|
||||
}); err != nil {
|
||||
atomic.StoreUint32(&retryTimeout, 1)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := cn.WithReader(c.context(ctx), c.cmdTimeout(cmd), cmd.readReply); err != nil {
|
||||
if cmd.readTimeout() == nil {
|
||||
atomic.StoreUint32(&retryTimeout, 1)
|
||||
} else {
|
||||
atomic.StoreUint32(&retryTimeout, 0)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
retry := shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1)
|
||||
return retry, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (c *baseClient) retryBackoff(attempt int) time.Duration {
|
||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
|
||||
}
|
||||
|
||||
func (c *baseClient) cmdTimeout(cmd Cmder) time.Duration {
|
||||
if timeout := cmd.readTimeout(); timeout != nil {
|
||||
t := *timeout
|
||||
if t == 0 {
|
||||
return 0
|
||||
}
|
||||
return t + 10*time.Second
|
||||
}
|
||||
return c.opt.ReadTimeout
|
||||
}
|
||||
|
||||
// Close closes the client, releasing any open resources.
|
||||
//
|
||||
// It is rare to Close a Client, as the Client is meant to be
|
||||
// long-lived and shared between many goroutines.
|
||||
func (c *baseClient) Close() error {
|
||||
var firstErr error
|
||||
if c.onClose != nil {
|
||||
if err := c.onClose(); err != nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
if err := c.connPool.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (c *baseClient) getAddr() string {
|
||||
return c.opt.Addr
|
||||
}
|
||||
|
||||
func (c *baseClient) processPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
if err := c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmdsFirstErr(cmds)
|
||||
}
|
||||
|
||||
func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
if err := c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmdsFirstErr(cmds)
|
||||
}
|
||||
|
||||
type pipelineProcessor func(context.Context, *pool.Conn, []Cmder) (bool, error)
|
||||
|
||||
func (c *baseClient) generalProcessPipeline(
|
||||
ctx context.Context, cmds []Cmder, p pipelineProcessor,
|
||||
) error {
|
||||
var lastErr error
|
||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
|
||||
if attempt > 0 {
|
||||
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Enable retries by default to retry dial errors returned by withConn.
|
||||
canRetry := true
|
||||
lastErr = c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
||||
var err error
|
||||
canRetry, err = p(ctx, cn, cmds)
|
||||
return err
|
||||
})
|
||||
if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) {
|
||||
return lastErr
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (c *baseClient) pipelineProcessCmds(
|
||||
ctx context.Context, cn *pool.Conn, cmds []Cmder,
|
||||
) (bool, error) {
|
||||
if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmds(wr, cmds)
|
||||
}); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
if err := cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
return pipelineReadCmds(rd, cmds)
|
||||
}); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func pipelineReadCmds(rd *proto.Reader, cmds []Cmder) error {
|
||||
for i, cmd := range cmds {
|
||||
err := cmd.readReply(rd)
|
||||
cmd.SetErr(err)
|
||||
if err != nil && !isRedisError(err) {
|
||||
setCmdsErr(cmds[i+1:], err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Retry errors like "LOADING redis is loading the dataset in memory".
|
||||
return cmds[0].Err()
|
||||
}
|
||||
|
||||
func (c *baseClient) txPipelineProcessCmds(
|
||||
ctx context.Context, cn *pool.Conn, cmds []Cmder,
|
||||
) (bool, error) {
|
||||
if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||
return writeCmds(wr, cmds)
|
||||
}); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return true, err
|
||||
}
|
||||
|
||||
if err := cn.WithReader(c.context(ctx), c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
||||
statusCmd := cmds[0].(*StatusCmd)
|
||||
// Trim multi and exec.
|
||||
trimmedCmds := cmds[1 : len(cmds)-1]
|
||||
|
||||
if err := txPipelineReadQueued(rd, statusCmd, trimmedCmds); err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return pipelineReadCmds(rd, trimmedCmds)
|
||||
}); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func txPipelineReadQueued(rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) error {
|
||||
// Parse +OK.
|
||||
if err := statusCmd.readReply(rd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Parse +QUEUED.
|
||||
for range cmds {
|
||||
if err := statusCmd.readReply(rd); err != nil && !isRedisError(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse number of replies.
|
||||
line, err := rd.ReadLine()
|
||||
if err != nil {
|
||||
if err == Nil {
|
||||
err = TxFailedErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if line[0] != proto.RespArray {
|
||||
return fmt.Errorf("redis: expected '*', but got line %q", line)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *baseClient) context(ctx context.Context) context.Context {
|
||||
if c.opt.ContextTimeoutEnabled {
|
||||
return ctx
|
||||
}
|
||||
return context.Background()
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Client is a Redis client representing a pool of zero or more underlying connections.
|
||||
// It's safe for concurrent use by multiple goroutines.
|
||||
//
|
||||
// Client creates and frees connections automatically; it also maintains a free pool
|
||||
// of idle connections. You can control the pool size with Config.PoolSize option.
|
||||
type Client struct {
|
||||
*baseClient
|
||||
cmdable
|
||||
hooksMixin
|
||||
}
|
||||
|
||||
// NewClient returns a client to the Redis Server specified by Options.
|
||||
func NewClient(opt *Options) *Client {
|
||||
opt.init()
|
||||
|
||||
c := Client{
|
||||
baseClient: &baseClient{
|
||||
opt: opt,
|
||||
},
|
||||
}
|
||||
c.init()
|
||||
c.connPool = newConnPool(opt, c.dialHook)
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Client) init() {
|
||||
c.cmdable = c.Process
|
||||
c.initHooks(hooks{
|
||||
dial: c.baseClient.dial,
|
||||
process: c.baseClient.process,
|
||||
pipeline: c.baseClient.processPipeline,
|
||||
txPipeline: c.baseClient.processTxPipeline,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Client) WithTimeout(timeout time.Duration) *Client {
|
||||
clone := *c
|
||||
clone.baseClient = c.baseClient.withTimeout(timeout)
|
||||
clone.init()
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (c *Client) Conn() *Conn {
|
||||
return newConn(c.opt, pool.NewStickyConnPool(c.connPool))
|
||||
}
|
||||
|
||||
// Do create a Cmd from the args and processes the cmd.
|
||||
func (c *Client) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c.Process(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *Client) Process(ctx context.Context, cmd Cmder) error {
|
||||
err := c.processHook(ctx, cmd)
|
||||
cmd.SetErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Options returns read-only Options that were used to create the client.
|
||||
func (c *Client) Options() *Options {
|
||||
return c.opt
|
||||
}
|
||||
|
||||
type PoolStats pool.Stats
|
||||
|
||||
// PoolStats returns connection pool stats.
|
||||
func (c *Client) PoolStats() *PoolStats {
|
||||
stats := c.connPool.Stats()
|
||||
return (*PoolStats)(stats)
|
||||
}
|
||||
|
||||
func (c *Client) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.Pipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
func (c *Client) Pipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
exec: pipelineExecer(c.processPipelineHook),
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Client) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.TxPipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
||||
func (c *Client) TxPipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
return c.processTxPipelineHook(ctx, cmds)
|
||||
},
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Client) pubSub() *PubSub {
|
||||
pubsub := &PubSub{
|
||||
opt: c.opt,
|
||||
|
||||
newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) {
|
||||
return c.newConn(ctx)
|
||||
},
|
||||
closeConn: c.connPool.CloseConn,
|
||||
}
|
||||
pubsub.init()
|
||||
return pubsub
|
||||
}
|
||||
|
||||
// Subscribe subscribes the client to the specified channels.
|
||||
// Channels can be omitted to create empty subscription.
|
||||
// Note that this method does not wait on a response from Redis, so the
|
||||
// subscription may not be active immediately. To force the connection to wait,
|
||||
// you may call the Receive() method on the returned *PubSub like so:
|
||||
//
|
||||
// sub := client.Subscribe(queryResp)
|
||||
// iface, err := sub.Receive()
|
||||
// if err != nil {
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// // Should be *Subscription, but others are possible if other actions have been
|
||||
// // taken on sub since it was created.
|
||||
// switch iface.(type) {
|
||||
// case *Subscription:
|
||||
// // subscribe succeeded
|
||||
// case *Message:
|
||||
// // received first message
|
||||
// case *Pong:
|
||||
// // pong received
|
||||
// default:
|
||||
// // handle error
|
||||
// }
|
||||
//
|
||||
// ch := sub.Channel()
|
||||
func (c *Client) Subscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
pubsub := c.pubSub()
|
||||
if len(channels) > 0 {
|
||||
_ = pubsub.Subscribe(ctx, channels...)
|
||||
}
|
||||
return pubsub
|
||||
}
|
||||
|
||||
// PSubscribe subscribes the client to the given patterns.
|
||||
// Patterns can be omitted to create empty subscription.
|
||||
func (c *Client) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
pubsub := c.pubSub()
|
||||
if len(channels) > 0 {
|
||||
_ = pubsub.PSubscribe(ctx, channels...)
|
||||
}
|
||||
return pubsub
|
||||
}
|
||||
|
||||
// SSubscribe Subscribes the client to the specified shard channels.
|
||||
// Channels can be omitted to create empty subscription.
|
||||
func (c *Client) SSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
pubsub := c.pubSub()
|
||||
if len(channels) > 0 {
|
||||
_ = pubsub.SSubscribe(ctx, channels...)
|
||||
}
|
||||
return pubsub
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Conn represents a single Redis connection rather than a pool of connections.
|
||||
// Prefer running commands from Client unless there is a specific need
|
||||
// for a continuous single Redis connection.
|
||||
type Conn struct {
|
||||
baseClient
|
||||
cmdable
|
||||
statefulCmdable
|
||||
hooksMixin
|
||||
}
|
||||
|
||||
func newConn(opt *Options, connPool pool.Pooler) *Conn {
|
||||
c := Conn{
|
||||
baseClient: baseClient{
|
||||
opt: opt,
|
||||
connPool: connPool,
|
||||
},
|
||||
}
|
||||
|
||||
c.cmdable = c.Process
|
||||
c.statefulCmdable = c.Process
|
||||
c.initHooks(hooks{
|
||||
dial: c.baseClient.dial,
|
||||
process: c.baseClient.process,
|
||||
pipeline: c.baseClient.processPipeline,
|
||||
txPipeline: c.baseClient.processTxPipeline,
|
||||
})
|
||||
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Conn) Process(ctx context.Context, cmd Cmder) error {
|
||||
err := c.processHook(ctx, cmd)
|
||||
cmd.SetErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.Pipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
func (c *Conn) Pipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
exec: c.processPipelineHook,
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Conn) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.TxPipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
||||
func (c *Conn) TxPipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
return c.processTxPipelineHook(ctx, cmds)
|
||||
},
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
Generated
Vendored
+15
-7
@@ -82,17 +82,17 @@ func NewBoolSliceResult(val []bool, err error) *BoolSliceCmd {
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// NewStringStringMapResult returns a StringStringMapCmd initialised with val and err for testing.
|
||||
func NewStringStringMapResult(val map[string]string, err error) *StringStringMapCmd {
|
||||
var cmd StringStringMapCmd
|
||||
// NewMapStringStringResult returns a MapStringStringCmd initialised with val and err for testing.
|
||||
func NewMapStringStringResult(val map[string]string, err error) *MapStringStringCmd {
|
||||
var cmd MapStringStringCmd
|
||||
cmd.val = val
|
||||
cmd.SetErr(err)
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// NewStringIntMapCmdResult returns a StringIntMapCmd initialised with val and err for testing.
|
||||
func NewStringIntMapCmdResult(val map[string]int64, err error) *StringIntMapCmd {
|
||||
var cmd StringIntMapCmd
|
||||
// NewMapStringIntCmdResult returns a MapStringIntCmd initialised with val and err for testing.
|
||||
func NewMapStringIntCmdResult(val map[string]int64, err error) *MapStringIntCmd {
|
||||
var cmd MapStringIntCmd
|
||||
cmd.val = val
|
||||
cmd.SetErr(err)
|
||||
return &cmd
|
||||
@@ -114,7 +114,7 @@ func NewZSliceCmdResult(val []Z, err error) *ZSliceCmd {
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// NewZWithKeyCmdResult returns a NewZWithKeyCmd initialised with val and err for testing.
|
||||
// NewZWithKeyCmdResult returns a ZWithKeyCmd initialised with val and err for testing.
|
||||
func NewZWithKeyCmdResult(val *ZWithKey, err error) *ZWithKeyCmd {
|
||||
var cmd ZWithKeyCmd
|
||||
cmd.val = val
|
||||
@@ -178,3 +178,11 @@ func NewXStreamSliceCmdResult(val []XStream, err error) *XStreamSliceCmd {
|
||||
cmd.SetErr(err)
|
||||
return &cmd
|
||||
}
|
||||
|
||||
// NewXPendingResult returns a XPendingCmd initialised with val and err for testing.
|
||||
func NewXPendingResult(val *XPending, err error) *XPendingCmd {
|
||||
var cmd XPendingCmd
|
||||
cmd.val = val
|
||||
cmd.SetErr(err)
|
||||
return &cmd
|
||||
}
|
||||
Generated
Vendored
+287
-196
@@ -12,12 +12,12 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
rendezvous "github.com/dgryski/go-rendezvous" //nolint
|
||||
"github.com/dgryski/go-rendezvous" //nolint
|
||||
|
||||
"github.com/go-redis/redis/v8/internal"
|
||||
"github.com/go-redis/redis/v8/internal/hashtag"
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/go-redis/redis/v8/internal/rand"
|
||||
"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/rand"
|
||||
)
|
||||
|
||||
var errRingShardsDown = errors.New("redis: all ring shards are down")
|
||||
@@ -48,8 +48,11 @@ type RingOptions struct {
|
||||
// Map of name => host:port addresses of ring shards.
|
||||
Addrs map[string]string
|
||||
|
||||
// NewClient creates a shard client with provided name and options.
|
||||
NewClient func(name string, opt *Options) *Client
|
||||
// NewClient creates a shard client with provided options.
|
||||
NewClient func(opt *Options) *Client
|
||||
|
||||
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
|
||||
ClientName string
|
||||
|
||||
// Frequency of PING commands sent to check shards availability.
|
||||
// Shard is considered down after 3 subsequent failed checks.
|
||||
@@ -67,6 +70,7 @@ type RingOptions struct {
|
||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
OnConnect func(ctx context.Context, cn *Conn) error
|
||||
|
||||
Protocol int
|
||||
Username string
|
||||
Password string
|
||||
DB int
|
||||
@@ -75,27 +79,32 @@ type RingOptions struct {
|
||||
MinRetryBackoff time.Duration
|
||||
MaxRetryBackoff time.Duration
|
||||
|
||||
DialTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
DialTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
ContextTimeoutEnabled bool
|
||||
|
||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
||||
PoolFIFO bool
|
||||
|
||||
PoolSize int
|
||||
MinIdleConns int
|
||||
MaxConnAge time.Duration
|
||||
PoolTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
IdleCheckFrequency time.Duration
|
||||
PoolSize int
|
||||
PoolTimeout time.Duration
|
||||
MinIdleConns int
|
||||
MaxIdleConns int
|
||||
MaxActiveConns int
|
||||
ConnMaxIdleTime time.Duration
|
||||
ConnMaxLifetime time.Duration
|
||||
|
||||
TLSConfig *tls.Config
|
||||
Limiter Limiter
|
||||
|
||||
DisableIndentity bool
|
||||
IdentitySuffix string
|
||||
}
|
||||
|
||||
func (opt *RingOptions) init() {
|
||||
if opt.NewClient == nil {
|
||||
opt.NewClient = func(name string, opt *Options) *Client {
|
||||
opt.NewClient = func(opt *Options) *Client {
|
||||
return NewClient(opt)
|
||||
}
|
||||
}
|
||||
@@ -129,29 +138,36 @@ func (opt *RingOptions) init() {
|
||||
|
||||
func (opt *RingOptions) clientOptions() *Options {
|
||||
return &Options{
|
||||
Dialer: opt.Dialer,
|
||||
OnConnect: opt.OnConnect,
|
||||
ClientName: opt.ClientName,
|
||||
Dialer: opt.Dialer,
|
||||
OnConnect: opt.OnConnect,
|
||||
|
||||
Protocol: opt.Protocol,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
DB: opt.DB,
|
||||
|
||||
MaxRetries: -1,
|
||||
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
|
||||
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxConnAge: opt.MaxConnAge,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
IdleTimeout: opt.IdleTimeout,
|
||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxIdleConns: opt.MaxIdleConns,
|
||||
MaxActiveConns: opt.MaxActiveConns,
|
||||
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||
|
||||
TLSConfig: opt.TLSConfig,
|
||||
Limiter: opt.Limiter,
|
||||
|
||||
DisableIndentity: opt.DisableIndentity,
|
||||
IdentitySuffix: opt.IdentitySuffix,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,14 +176,16 @@ func (opt *RingOptions) clientOptions() *Options {
|
||||
type ringShard struct {
|
||||
Client *Client
|
||||
down int32
|
||||
addr string
|
||||
}
|
||||
|
||||
func newRingShard(opt *RingOptions, name, addr string) *ringShard {
|
||||
func newRingShard(opt *RingOptions, addr string) *ringShard {
|
||||
clopt := opt.clientOptions()
|
||||
clopt.Addr = addr
|
||||
|
||||
return &ringShard{
|
||||
Client: opt.NewClient(name, clopt),
|
||||
Client: opt.NewClient(clopt),
|
||||
addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -208,161 +226,237 @@ func (shard *ringShard) Vote(up bool) bool {
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type ringShards struct {
|
||||
type ringSharding struct {
|
||||
opt *RingOptions
|
||||
|
||||
mu sync.RWMutex
|
||||
hash ConsistentHash
|
||||
shards map[string]*ringShard // read only
|
||||
list []*ringShard // read only
|
||||
numShard int
|
||||
closed bool
|
||||
mu sync.RWMutex
|
||||
shards *ringShards
|
||||
closed bool
|
||||
hash ConsistentHash
|
||||
numShard int
|
||||
onNewNode []func(rdb *Client)
|
||||
|
||||
// ensures exclusive access to SetAddrs so there is no need
|
||||
// to hold mu for the duration of potentially long shard creation
|
||||
setAddrsMu sync.Mutex
|
||||
}
|
||||
|
||||
func newRingShards(opt *RingOptions) *ringShards {
|
||||
shards := make(map[string]*ringShard, len(opt.Addrs))
|
||||
list := make([]*ringShard, 0, len(shards))
|
||||
type ringShards struct {
|
||||
m map[string]*ringShard
|
||||
list []*ringShard
|
||||
}
|
||||
|
||||
for name, addr := range opt.Addrs {
|
||||
shard := newRingShard(opt, name, addr)
|
||||
shards[name] = shard
|
||||
|
||||
list = append(list, shard)
|
||||
}
|
||||
|
||||
c := &ringShards{
|
||||
func newRingSharding(opt *RingOptions) *ringSharding {
|
||||
c := &ringSharding{
|
||||
opt: opt,
|
||||
|
||||
shards: shards,
|
||||
list: list,
|
||||
}
|
||||
c.rebalance()
|
||||
c.SetAddrs(opt.Addrs)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *ringShards) List() []*ringShard {
|
||||
func (c *ringSharding) OnNewNode(fn func(rdb *Client)) {
|
||||
c.mu.Lock()
|
||||
c.onNewNode = append(c.onNewNode, fn)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetAddrs replaces the shards in use, such that you can increase and
|
||||
// decrease number of shards, that you use. It will reuse shards that
|
||||
// existed before and close the ones that will not be used anymore.
|
||||
func (c *ringSharding) SetAddrs(addrs map[string]string) {
|
||||
c.setAddrsMu.Lock()
|
||||
defer c.setAddrsMu.Unlock()
|
||||
|
||||
cleanup := func(shards map[string]*ringShard) {
|
||||
for addr, shard := range shards {
|
||||
if err := shard.Client.Close(); err != nil {
|
||||
internal.Logger.Printf(context.Background(), "shard.Close %s failed: %s", addr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
if c.closed {
|
||||
c.mu.RUnlock()
|
||||
return
|
||||
}
|
||||
existing := c.shards
|
||||
c.mu.RUnlock()
|
||||
|
||||
shards, created, unused := c.newRingShards(addrs, existing)
|
||||
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
cleanup(created)
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.shards = shards
|
||||
c.rebalanceLocked()
|
||||
c.mu.Unlock()
|
||||
|
||||
cleanup(unused)
|
||||
}
|
||||
|
||||
func (c *ringSharding) newRingShards(
|
||||
addrs map[string]string, existing *ringShards,
|
||||
) (shards *ringShards, created, unused map[string]*ringShard) {
|
||||
shards = &ringShards{m: make(map[string]*ringShard, len(addrs))}
|
||||
created = make(map[string]*ringShard) // indexed by addr
|
||||
unused = make(map[string]*ringShard) // indexed by addr
|
||||
|
||||
if existing != nil {
|
||||
for _, shard := range existing.list {
|
||||
unused[shard.addr] = shard
|
||||
}
|
||||
}
|
||||
|
||||
for name, addr := range addrs {
|
||||
if shard, ok := unused[addr]; ok {
|
||||
shards.m[name] = shard
|
||||
delete(unused, addr)
|
||||
} else {
|
||||
shard := newRingShard(c.opt, addr)
|
||||
shards.m[name] = shard
|
||||
created[addr] = shard
|
||||
|
||||
for _, fn := range c.onNewNode {
|
||||
fn(shard.Client)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, shard := range shards.m {
|
||||
shards.list = append(shards.list, shard)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *ringSharding) List() []*ringShard {
|
||||
var list []*ringShard
|
||||
|
||||
c.mu.RLock()
|
||||
if !c.closed {
|
||||
list = c.list
|
||||
list = c.shards.list
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
func (c *ringShards) Hash(key string) string {
|
||||
func (c *ringSharding) Hash(key string) string {
|
||||
key = hashtag.Key(key)
|
||||
|
||||
var hash string
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.numShard > 0 {
|
||||
hash = c.hash.Get(key)
|
||||
}
|
||||
c.mu.RUnlock()
|
||||
|
||||
return hash
|
||||
}
|
||||
|
||||
func (c *ringShards) GetByKey(key string) (*ringShard, error) {
|
||||
func (c *ringSharding) GetByKey(key string) (*ringShard, error) {
|
||||
key = hashtag.Key(key)
|
||||
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.closed {
|
||||
c.mu.RUnlock()
|
||||
return nil, pool.ErrClosed
|
||||
}
|
||||
|
||||
if c.numShard == 0 {
|
||||
c.mu.RUnlock()
|
||||
return nil, errRingShardsDown
|
||||
}
|
||||
|
||||
hash := c.hash.Get(key)
|
||||
if hash == "" {
|
||||
c.mu.RUnlock()
|
||||
shardName := c.hash.Get(key)
|
||||
if shardName == "" {
|
||||
return nil, errRingShardsDown
|
||||
}
|
||||
|
||||
shard := c.shards[hash]
|
||||
c.mu.RUnlock()
|
||||
|
||||
return shard, nil
|
||||
return c.shards.m[shardName], nil
|
||||
}
|
||||
|
||||
func (c *ringShards) GetByName(shardName string) (*ringShard, error) {
|
||||
func (c *ringSharding) GetByName(shardName string) (*ringShard, error) {
|
||||
if shardName == "" {
|
||||
return c.Random()
|
||||
}
|
||||
|
||||
c.mu.RLock()
|
||||
shard := c.shards[shardName]
|
||||
c.mu.RUnlock()
|
||||
return shard, nil
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return c.shards.m[shardName], nil
|
||||
}
|
||||
|
||||
func (c *ringShards) Random() (*ringShard, error) {
|
||||
func (c *ringSharding) Random() (*ringShard, error) {
|
||||
return c.GetByKey(strconv.Itoa(rand.Int()))
|
||||
}
|
||||
|
||||
// heartbeat monitors state of each shard in the ring.
|
||||
func (c *ringShards) Heartbeat(frequency time.Duration) {
|
||||
// Heartbeat monitors state of each shard in the ring.
|
||||
func (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) {
|
||||
ticker := time.NewTicker(frequency)
|
||||
defer ticker.Stop()
|
||||
|
||||
ctx := context.Background()
|
||||
for range ticker.C {
|
||||
var rebalance bool
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
var rebalance bool
|
||||
|
||||
for _, shard := range c.List() {
|
||||
err := shard.Client.Ping(ctx).Err()
|
||||
isUp := err == nil || err == pool.ErrPoolTimeout
|
||||
if shard.Vote(isUp) {
|
||||
internal.Logger.Printf(context.Background(), "ring shard state changed: %s", shard)
|
||||
rebalance = true
|
||||
for _, shard := range c.List() {
|
||||
err := shard.Client.Ping(ctx).Err()
|
||||
isUp := err == nil || err == pool.ErrPoolTimeout
|
||||
if shard.Vote(isUp) {
|
||||
internal.Logger.Printf(ctx, "ring shard state changed: %s", shard)
|
||||
rebalance = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if rebalance {
|
||||
c.rebalance()
|
||||
if rebalance {
|
||||
c.mu.Lock()
|
||||
c.rebalanceLocked()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rebalance removes dead shards from the Ring.
|
||||
func (c *ringShards) rebalance() {
|
||||
c.mu.RLock()
|
||||
shards := c.shards
|
||||
c.mu.RUnlock()
|
||||
// rebalanceLocked removes dead shards from the Ring.
|
||||
// Requires c.mu locked.
|
||||
func (c *ringSharding) rebalanceLocked() {
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
if c.shards == nil {
|
||||
return
|
||||
}
|
||||
|
||||
liveShards := make([]string, 0, len(shards))
|
||||
liveShards := make([]string, 0, len(c.shards.m))
|
||||
|
||||
for name, shard := range shards {
|
||||
for name, shard := range c.shards.m {
|
||||
if shard.IsUp() {
|
||||
liveShards = append(liveShards, name)
|
||||
}
|
||||
}
|
||||
|
||||
hash := c.opt.NewConsistentHash(liveShards)
|
||||
|
||||
c.mu.Lock()
|
||||
c.hash = hash
|
||||
c.hash = c.opt.NewConsistentHash(liveShards)
|
||||
c.numShard = len(liveShards)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *ringShards) Len() int {
|
||||
func (c *ringSharding) Len() int {
|
||||
c.mu.RLock()
|
||||
l := c.numShard
|
||||
c.mu.RUnlock()
|
||||
return l
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return c.numShard
|
||||
}
|
||||
|
||||
func (c *ringShards) Close() error {
|
||||
func (c *ringSharding) Close() error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -372,26 +466,22 @@ func (c *ringShards) Close() error {
|
||||
c.closed = true
|
||||
|
||||
var firstErr error
|
||||
for _, shard := range c.shards {
|
||||
|
||||
for _, shard := range c.shards.list {
|
||||
if err := shard.Client.Close(); err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
}
|
||||
|
||||
c.hash = nil
|
||||
c.shards = nil
|
||||
c.list = nil
|
||||
c.numShard = 0
|
||||
|
||||
return firstErr
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
type ring struct {
|
||||
opt *RingOptions
|
||||
shards *ringShards
|
||||
cmdsInfoCache *cmdsInfoCache //nolint:structcheck
|
||||
}
|
||||
|
||||
// Ring is a Redis client that uses consistent hashing to distribute
|
||||
// keys across multiple Redis servers (shards). It's safe for
|
||||
// concurrent use by multiple goroutines.
|
||||
@@ -407,47 +497,49 @@ type ring struct {
|
||||
// and can tolerate losing data when one of the servers dies.
|
||||
// Otherwise you should use Redis Cluster.
|
||||
type Ring struct {
|
||||
*ring
|
||||
cmdable
|
||||
hooks
|
||||
ctx context.Context
|
||||
hooksMixin
|
||||
|
||||
opt *RingOptions
|
||||
sharding *ringSharding
|
||||
cmdsInfoCache *cmdsInfoCache
|
||||
heartbeatCancelFn context.CancelFunc
|
||||
}
|
||||
|
||||
func NewRing(opt *RingOptions) *Ring {
|
||||
opt.init()
|
||||
|
||||
hbCtx, hbCancel := context.WithCancel(context.Background())
|
||||
|
||||
ring := Ring{
|
||||
ring: &ring{
|
||||
opt: opt,
|
||||
shards: newRingShards(opt),
|
||||
},
|
||||
ctx: context.Background(),
|
||||
opt: opt,
|
||||
sharding: newRingSharding(opt),
|
||||
heartbeatCancelFn: hbCancel,
|
||||
}
|
||||
|
||||
ring.cmdsInfoCache = newCmdsInfoCache(ring.cmdsInfo)
|
||||
ring.cmdable = ring.Process
|
||||
|
||||
go ring.shards.Heartbeat(opt.HeartbeatFrequency)
|
||||
ring.initHooks(hooks{
|
||||
process: ring.process,
|
||||
pipeline: func(ctx context.Context, cmds []Cmder) error {
|
||||
return ring.generalProcessPipeline(ctx, cmds, false)
|
||||
},
|
||||
txPipeline: func(ctx context.Context, cmds []Cmder) error {
|
||||
return ring.generalProcessPipeline(ctx, cmds, true)
|
||||
},
|
||||
})
|
||||
|
||||
go ring.sharding.Heartbeat(hbCtx, opt.HeartbeatFrequency)
|
||||
|
||||
return &ring
|
||||
}
|
||||
|
||||
func (c *Ring) Context() context.Context {
|
||||
return c.ctx
|
||||
func (c *Ring) SetAddrs(addrs map[string]string) {
|
||||
c.sharding.SetAddrs(addrs)
|
||||
}
|
||||
|
||||
func (c *Ring) WithContext(ctx context.Context) *Ring {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
clone := *c
|
||||
clone.cmdable = clone.Process
|
||||
clone.hooks.lock()
|
||||
clone.ctx = ctx
|
||||
return &clone
|
||||
}
|
||||
|
||||
// Do creates a Cmd from the args and processes the cmd.
|
||||
// Do create a Cmd from the args and processes the cmd.
|
||||
func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
cmd := NewCmd(ctx, args...)
|
||||
_ = c.Process(ctx, cmd)
|
||||
@@ -455,7 +547,9 @@ func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||
}
|
||||
|
||||
func (c *Ring) Process(ctx context.Context, cmd Cmder) error {
|
||||
return c.hooks.process(ctx, cmd, c.process)
|
||||
err := c.processHook(ctx, cmd)
|
||||
cmd.SetErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Options returns read-only Options that were used to create the client.
|
||||
@@ -469,7 +563,7 @@ func (c *Ring) retryBackoff(attempt int) time.Duration {
|
||||
|
||||
// PoolStats returns accumulated connection pool stats.
|
||||
func (c *Ring) PoolStats() *PoolStats {
|
||||
shards := c.shards.List()
|
||||
shards := c.sharding.List()
|
||||
var acc PoolStats
|
||||
for _, shard := range shards {
|
||||
s := shard.Client.connPool.Stats()
|
||||
@@ -484,7 +578,7 @@ func (c *Ring) PoolStats() *PoolStats {
|
||||
|
||||
// Len returns the current number of shards in the ring.
|
||||
func (c *Ring) Len() int {
|
||||
return c.shards.Len()
|
||||
return c.sharding.Len()
|
||||
}
|
||||
|
||||
// Subscribe subscribes the client to the specified channels.
|
||||
@@ -493,7 +587,7 @@ func (c *Ring) Subscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
panic("at least one channel is required")
|
||||
}
|
||||
|
||||
shard, err := c.shards.GetByKey(channels[0])
|
||||
shard, err := c.sharding.GetByKey(channels[0])
|
||||
if err != nil {
|
||||
// TODO: return PubSub with sticky error
|
||||
panic(err)
|
||||
@@ -507,7 +601,7 @@ func (c *Ring) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
panic("at least one channel is required")
|
||||
}
|
||||
|
||||
shard, err := c.shards.GetByKey(channels[0])
|
||||
shard, err := c.sharding.GetByKey(channels[0])
|
||||
if err != nil {
|
||||
// TODO: return PubSub with sticky error
|
||||
panic(err)
|
||||
@@ -515,13 +609,30 @@ func (c *Ring) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
return shard.Client.PSubscribe(ctx, channels...)
|
||||
}
|
||||
|
||||
// SSubscribe Subscribes the client to the specified shard channels.
|
||||
func (c *Ring) SSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||
if len(channels) == 0 {
|
||||
panic("at least one channel is required")
|
||||
}
|
||||
shard, err := c.sharding.GetByKey(channels[0])
|
||||
if err != nil {
|
||||
// TODO: return PubSub with sticky error
|
||||
panic(err)
|
||||
}
|
||||
return shard.Client.SSubscribe(ctx, channels...)
|
||||
}
|
||||
|
||||
func (c *Ring) OnNewNode(fn func(rdb *Client)) {
|
||||
c.sharding.OnNewNode(fn)
|
||||
}
|
||||
|
||||
// ForEachShard concurrently calls the fn on each live shard in the ring.
|
||||
// It returns the first error if any.
|
||||
func (c *Ring) ForEachShard(
|
||||
ctx context.Context,
|
||||
fn func(ctx context.Context, client *Client) error,
|
||||
) error {
|
||||
shards := c.shards.List()
|
||||
shards := c.sharding.List()
|
||||
var wg sync.WaitGroup
|
||||
errCh := make(chan error, 1)
|
||||
for _, shard := range shards {
|
||||
@@ -552,7 +663,7 @@ func (c *Ring) ForEachShard(
|
||||
}
|
||||
|
||||
func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) {
|
||||
shards := c.shards.List()
|
||||
shards := c.sharding.List()
|
||||
var firstErr error
|
||||
for _, shard := range shards {
|
||||
cmdsInfo, err := shard.Client.Command(ctx).Result()
|
||||
@@ -569,26 +680,13 @@ func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) {
|
||||
return nil, firstErr
|
||||
}
|
||||
|
||||
func (c *Ring) cmdInfo(ctx context.Context, name string) *CommandInfo {
|
||||
cmdsInfo, err := c.cmdsInfoCache.Get(ctx)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
info := cmdsInfo[name]
|
||||
if info == nil {
|
||||
internal.Logger.Printf(ctx, "info for cmd=%s not found", name)
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func (c *Ring) cmdShard(ctx context.Context, cmd Cmder) (*ringShard, error) {
|
||||
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||
pos := cmdFirstKeyPos(cmd, cmdInfo)
|
||||
pos := cmdFirstKeyPos(cmd)
|
||||
if pos == 0 {
|
||||
return c.shards.Random()
|
||||
return c.sharding.Random()
|
||||
}
|
||||
firstKey := cmd.stringArg(pos)
|
||||
return c.shards.GetByKey(firstKey)
|
||||
return c.sharding.GetByKey(firstKey)
|
||||
}
|
||||
|
||||
func (c *Ring) process(ctx context.Context, cmd Cmder) error {
|
||||
@@ -619,47 +717,41 @@ func (c *Ring) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder
|
||||
|
||||
func (c *Ring) Pipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processPipeline,
|
||||
exec: pipelineExecer(c.processPipelineHook),
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Ring) processPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
||||
return c.generalProcessPipeline(ctx, cmds, false)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Ring) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||
return c.TxPipeline().Pipelined(ctx, fn)
|
||||
}
|
||||
|
||||
func (c *Ring) TxPipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: c.processTxPipeline,
|
||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
return c.processTxPipelineHook(ctx, cmds)
|
||||
},
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func (c *Ring) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
||||
return c.generalProcessPipeline(ctx, cmds, true)
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Ring) generalProcessPipeline(
|
||||
ctx context.Context, cmds []Cmder, tx bool,
|
||||
) error {
|
||||
if tx {
|
||||
// Trim multi .. exec.
|
||||
cmds = cmds[1 : len(cmds)-1]
|
||||
}
|
||||
|
||||
cmdsMap := make(map[string][]Cmder)
|
||||
|
||||
for _, cmd := range cmds {
|
||||
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||
hash := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo))
|
||||
hash := cmd.stringArg(cmdFirstKeyPos(cmd))
|
||||
if hash != "" {
|
||||
hash = c.shards.Hash(hash)
|
||||
hash = c.sharding.Hash(hash)
|
||||
}
|
||||
cmdsMap[hash] = append(cmdsMap[hash], cmd)
|
||||
}
|
||||
@@ -670,7 +762,19 @@ func (c *Ring) generalProcessPipeline(
|
||||
go func(hash string, cmds []Cmder) {
|
||||
defer wg.Done()
|
||||
|
||||
_ = c.processShardPipeline(ctx, hash, cmds, tx)
|
||||
// TODO: retry?
|
||||
shard, err := c.sharding.GetByName(hash)
|
||||
if err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return
|
||||
}
|
||||
|
||||
if tx {
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
_ = shard.Client.processTxPipelineHook(ctx, cmds)
|
||||
} else {
|
||||
_ = shard.Client.processPipelineHook(ctx, cmds)
|
||||
}
|
||||
}(hash, cmds)
|
||||
}
|
||||
|
||||
@@ -678,31 +782,16 @@ func (c *Ring) generalProcessPipeline(
|
||||
return cmdsFirstErr(cmds)
|
||||
}
|
||||
|
||||
func (c *Ring) processShardPipeline(
|
||||
ctx context.Context, hash string, cmds []Cmder, tx bool,
|
||||
) error {
|
||||
// TODO: retry?
|
||||
shard, err := c.shards.GetByName(hash)
|
||||
if err != nil {
|
||||
setCmdsErr(cmds, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if tx {
|
||||
return shard.Client.processTxPipeline(ctx, cmds)
|
||||
}
|
||||
return shard.Client.processPipeline(ctx, cmds)
|
||||
}
|
||||
|
||||
func (c *Ring) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
|
||||
if len(keys) == 0 {
|
||||
return fmt.Errorf("redis: Watch requires at least one key")
|
||||
}
|
||||
|
||||
var shards []*ringShard
|
||||
|
||||
for _, key := range keys {
|
||||
if key != "" {
|
||||
shard, err := c.shards.GetByKey(hashtag.Key(key))
|
||||
shard, err := c.sharding.GetByKey(hashtag.Key(key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -732,5 +821,7 @@ func (c *Ring) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) er
|
||||
// It is rare to Close a Ring, as the Ring is meant to be long-lived
|
||||
// and shared between many goroutines.
|
||||
func (c *Ring) Close() error {
|
||||
return c.shards.Close()
|
||||
c.heartbeatCancelFn()
|
||||
|
||||
return c.sharding.Close()
|
||||
}
|
||||
Generated
Vendored
+21
-2
@@ -5,12 +5,13 @@ import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Scripter interface {
|
||||
Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd
|
||||
EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd
|
||||
EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd
|
||||
EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd
|
||||
ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd
|
||||
ScriptLoad(ctx context.Context, script string) *StringCmd
|
||||
}
|
||||
@@ -50,16 +51,34 @@ func (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...in
|
||||
return c.Eval(ctx, s.src, keys, args...)
|
||||
}
|
||||
|
||||
func (s *Script) EvalRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
||||
return c.EvalRO(ctx, s.src, keys, args...)
|
||||
}
|
||||
|
||||
func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
||||
return c.EvalSha(ctx, s.hash, keys, args...)
|
||||
}
|
||||
|
||||
func (s *Script) EvalShaRO(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
||||
return c.EvalShaRO(ctx, s.hash, keys, args...)
|
||||
}
|
||||
|
||||
// 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 err := r.Err(); err != nil && strings.HasPrefix(err.Error(), "NOSCRIPT ") {
|
||||
if HasErrorPrefix(r.Err(), "NOSCRIPT") {
|
||||
return s.Eval(ctx, c, keys, args...)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// RunRO optimistically uses EVALSHA_RO to run the script. If script does not exist
|
||||
// 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") {
|
||||
return s.EvalRO(ctx, c, keys, args...)
|
||||
}
|
||||
return r
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
type ScriptingFunctionsCmdable interface {
|
||||
Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd
|
||||
EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd
|
||||
EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd
|
||||
EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd
|
||||
ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd
|
||||
ScriptFlush(ctx context.Context) *StatusCmd
|
||||
ScriptKill(ctx context.Context) *StatusCmd
|
||||
ScriptLoad(ctx context.Context, script string) *StringCmd
|
||||
|
||||
FunctionLoad(ctx context.Context, code string) *StringCmd
|
||||
FunctionLoadReplace(ctx context.Context, code string) *StringCmd
|
||||
FunctionDelete(ctx context.Context, libName string) *StringCmd
|
||||
FunctionFlush(ctx context.Context) *StringCmd
|
||||
FunctionKill(ctx context.Context) *StringCmd
|
||||
FunctionFlushAsync(ctx context.Context) *StringCmd
|
||||
FunctionList(ctx context.Context, q FunctionListQuery) *FunctionListCmd
|
||||
FunctionDump(ctx context.Context) *StringCmd
|
||||
FunctionRestore(ctx context.Context, libDump string) *StringCmd
|
||||
FunctionStats(ctx context.Context) *FunctionStatsCmd
|
||||
FCall(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd
|
||||
FCallRo(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd
|
||||
FCallRO(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd {
|
||||
return c.eval(ctx, "eval", script, keys, args...)
|
||||
}
|
||||
|
||||
func (c cmdable) EvalRO(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd {
|
||||
return c.eval(ctx, "eval_ro", script, keys, args...)
|
||||
}
|
||||
|
||||
func (c cmdable) EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd {
|
||||
return c.eval(ctx, "evalsha", sha1, keys, args...)
|
||||
}
|
||||
|
||||
func (c cmdable) EvalShaRO(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd {
|
||||
return c.eval(ctx, "evalsha_ro", sha1, keys, args...)
|
||||
}
|
||||
|
||||
func (c cmdable) eval(ctx context.Context, name, payload string, keys []string, args ...interface{}) *Cmd {
|
||||
cmdArgs := make([]interface{}, 3+len(keys), 3+len(keys)+len(args))
|
||||
cmdArgs[0] = name
|
||||
cmdArgs[1] = payload
|
||||
cmdArgs[2] = len(keys)
|
||||
for i, key := range keys {
|
||||
cmdArgs[3+i] = key
|
||||
}
|
||||
cmdArgs = appendArgs(cmdArgs, args)
|
||||
cmd := NewCmd(ctx, cmdArgs...)
|
||||
|
||||
// it is possible that only args exist without a key.
|
||||
// rdb.eval(ctx, eval, script, nil, arg1, arg2)
|
||||
if len(keys) > 0 {
|
||||
cmd.SetFirstKeyPos(3)
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd {
|
||||
args := make([]interface{}, 2+len(hashes))
|
||||
args[0] = "script"
|
||||
args[1] = "exists"
|
||||
for i, hash := range hashes {
|
||||
args[2+i] = hash
|
||||
}
|
||||
cmd := NewBoolSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ScriptFlush(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "script", "flush")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ScriptKill(ctx context.Context) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "script", "kill")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ScriptLoad(ctx context.Context, script string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "script", "load", script)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
// FunctionListQuery is used with FunctionList to query for Redis libraries
|
||||
//
|
||||
// LibraryNamePattern - Use an empty string to get all libraries.
|
||||
// - Use a glob-style pattern to match multiple libraries with a matching name
|
||||
// - Use a library's full name to match a single library
|
||||
// WithCode - If true, it will return the code of the library
|
||||
type FunctionListQuery struct {
|
||||
LibraryNamePattern string
|
||||
WithCode bool
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionLoad(ctx context.Context, code string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "load", code)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionLoadReplace(ctx context.Context, code string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "load", "replace", code)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionDelete(ctx context.Context, libName string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "delete", libName)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionFlush(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "flush")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionKill(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "kill")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionFlushAsync(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "flush", "async")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionList(ctx context.Context, q FunctionListQuery) *FunctionListCmd {
|
||||
args := make([]interface{}, 2, 5)
|
||||
args[0] = "function"
|
||||
args[1] = "list"
|
||||
if q.LibraryNamePattern != "" {
|
||||
args = append(args, "libraryname", q.LibraryNamePattern)
|
||||
}
|
||||
if q.WithCode {
|
||||
args = append(args, "withcode")
|
||||
}
|
||||
cmd := NewFunctionListCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionDump(ctx context.Context) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "dump")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionRestore(ctx context.Context, libDump string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "function", "restore", libDump)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FunctionStats(ctx context.Context) *FunctionStatsCmd {
|
||||
cmd := NewFunctionStatsCmd(ctx, "function", "stats")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) FCall(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd {
|
||||
cmdArgs := fcallArgs("fcall", function, keys, args...)
|
||||
cmd := NewCmd(ctx, cmdArgs...)
|
||||
if len(keys) > 0 {
|
||||
cmd.SetFirstKeyPos(3)
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// FCallRo this function simply calls FCallRO,
|
||||
// Deprecated: to maintain convention FCallRO.
|
||||
func (c cmdable) FCallRo(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd {
|
||||
return c.FCallRO(ctx, function, keys, args...)
|
||||
}
|
||||
|
||||
func (c cmdable) FCallRO(ctx context.Context, function string, keys []string, args ...interface{}) *Cmd {
|
||||
cmdArgs := fcallArgs("fcall_ro", function, keys, args...)
|
||||
cmd := NewCmd(ctx, cmdArgs...)
|
||||
if len(keys) > 0 {
|
||||
cmd.SetFirstKeyPos(3)
|
||||
}
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func fcallArgs(command string, function string, keys []string, args ...interface{}) []interface{} {
|
||||
cmdArgs := make([]interface{}, 3+len(keys), 3+len(keys)+len(args))
|
||||
cmdArgs[0] = command
|
||||
cmdArgs[1] = function
|
||||
cmdArgs[2] = len(keys)
|
||||
for i, key := range keys {
|
||||
cmdArgs[3+i] = key
|
||||
}
|
||||
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
return cmdArgs
|
||||
}
|
||||
Generated
Vendored
+200
-164
@@ -9,9 +9,9 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal"
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/go-redis/redis/v8/internal/rand"
|
||||
"github.com/redis/go-redis/v9/internal"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/rand"
|
||||
)
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
@@ -24,6 +24,9 @@ type FailoverOptions struct {
|
||||
// A seed list of host:port addresses of sentinel nodes.
|
||||
SentinelAddrs []string
|
||||
|
||||
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
|
||||
ClientName string
|
||||
|
||||
// If specified with SentinelPassword, enables ACL-based authentication (via
|
||||
// AUTH <user> <pass>).
|
||||
SentinelUsername string
|
||||
@@ -32,25 +35,26 @@ type FailoverOptions struct {
|
||||
// authentication.
|
||||
SentinelPassword string
|
||||
|
||||
// Allows routing read-only commands to the closest master or slave node.
|
||||
// Allows routing read-only commands to the closest master or replica node.
|
||||
// This option only works with NewFailoverClusterClient.
|
||||
RouteByLatency bool
|
||||
// Allows routing read-only commands to the random master or slave node.
|
||||
// Allows routing read-only commands to the random master or replica node.
|
||||
// This option only works with NewFailoverClusterClient.
|
||||
RouteRandomly bool
|
||||
|
||||
// Route all commands to slave read-only nodes.
|
||||
SlaveOnly bool
|
||||
// Route all commands to replica read-only nodes.
|
||||
ReplicaOnly bool
|
||||
|
||||
// Use slaves disconnected with master when cannot get connected slaves
|
||||
// Now, this option only works in RandomSlaveAddr function.
|
||||
UseDisconnectedSlaves bool
|
||||
// Use replicas disconnected with master when cannot get connected replicas
|
||||
// Now, this option only works in RandomReplicaAddr function.
|
||||
UseDisconnectedReplicas bool
|
||||
|
||||
// Following options are copied from Options struct.
|
||||
|
||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
OnConnect func(ctx context.Context, cn *Conn) error
|
||||
|
||||
Protocol int
|
||||
Username string
|
||||
Password string
|
||||
DB int
|
||||
@@ -59,31 +63,37 @@ type FailoverOptions struct {
|
||||
MinRetryBackoff time.Duration
|
||||
MaxRetryBackoff time.Duration
|
||||
|
||||
DialTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
DialTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
ContextTimeoutEnabled bool
|
||||
|
||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
||||
PoolFIFO bool
|
||||
|
||||
PoolSize int
|
||||
MinIdleConns int
|
||||
MaxConnAge time.Duration
|
||||
PoolTimeout time.Duration
|
||||
IdleTimeout time.Duration
|
||||
IdleCheckFrequency time.Duration
|
||||
PoolSize int
|
||||
PoolTimeout time.Duration
|
||||
MinIdleConns int
|
||||
MaxIdleConns int
|
||||
MaxActiveConns int
|
||||
ConnMaxIdleTime time.Duration
|
||||
ConnMaxLifetime time.Duration
|
||||
|
||||
TLSConfig *tls.Config
|
||||
|
||||
DisableIndentity bool
|
||||
IdentitySuffix string
|
||||
}
|
||||
|
||||
func (opt *FailoverOptions) clientOptions() *Options {
|
||||
return &Options{
|
||||
Addr: "FailoverClient",
|
||||
Addr: "FailoverClient",
|
||||
ClientName: opt.ClientName,
|
||||
|
||||
Dialer: opt.Dialer,
|
||||
OnConnect: opt.OnConnect,
|
||||
|
||||
DB: opt.DB,
|
||||
Protocol: opt.Protocol,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
|
||||
@@ -91,25 +101,31 @@ func (opt *FailoverOptions) clientOptions() *Options {
|
||||
MinRetryBackoff: opt.MinRetryBackoff,
|
||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
|
||||
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
IdleTimeout: opt.IdleTimeout,
|
||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxConnAge: opt.MaxConnAge,
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxIdleConns: opt.MaxIdleConns,
|
||||
MaxActiveConns: opt.MaxActiveConns,
|
||||
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||
|
||||
TLSConfig: opt.TLSConfig,
|
||||
|
||||
DisableIndentity: opt.DisableIndentity,
|
||||
IdentitySuffix: opt.IdentitySuffix,
|
||||
}
|
||||
}
|
||||
|
||||
func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
||||
return &Options{
|
||||
Addr: addr,
|
||||
Addr: addr,
|
||||
ClientName: opt.ClientName,
|
||||
|
||||
Dialer: opt.Dialer,
|
||||
OnConnect: opt.OnConnect,
|
||||
@@ -122,17 +138,19 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
||||
MinRetryBackoff: opt.MinRetryBackoff,
|
||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
|
||||
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
IdleTimeout: opt.IdleTimeout,
|
||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxConnAge: opt.MaxConnAge,
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxIdleConns: opt.MaxIdleConns,
|
||||
MaxActiveConns: opt.MaxActiveConns,
|
||||
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||
|
||||
TLSConfig: opt.TLSConfig,
|
||||
}
|
||||
@@ -140,9 +158,12 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
||||
|
||||
func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
|
||||
return &ClusterOptions{
|
||||
ClientName: opt.ClientName,
|
||||
|
||||
Dialer: opt.Dialer,
|
||||
OnConnect: opt.OnConnect,
|
||||
|
||||
Protocol: opt.Protocol,
|
||||
Username: opt.Username,
|
||||
Password: opt.Password,
|
||||
|
||||
@@ -154,17 +175,19 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
|
||||
MinRetryBackoff: opt.MinRetryBackoff,
|
||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
DialTimeout: opt.DialTimeout,
|
||||
ReadTimeout: opt.ReadTimeout,
|
||||
WriteTimeout: opt.WriteTimeout,
|
||||
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
|
||||
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
IdleTimeout: opt.IdleTimeout,
|
||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxConnAge: opt.MaxConnAge,
|
||||
PoolFIFO: opt.PoolFIFO,
|
||||
PoolSize: opt.PoolSize,
|
||||
PoolTimeout: opt.PoolTimeout,
|
||||
MinIdleConns: opt.MinIdleConns,
|
||||
MaxIdleConns: opt.MaxIdleConns,
|
||||
MaxActiveConns: opt.MaxActiveConns,
|
||||
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||
|
||||
TLSConfig: opt.TLSConfig,
|
||||
}
|
||||
@@ -194,10 +217,21 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
|
||||
}
|
||||
|
||||
opt := failoverOpt.clientOptions()
|
||||
opt.Dialer = masterSlaveDialer(failover)
|
||||
opt.Dialer = masterReplicaDialer(failover)
|
||||
opt.init()
|
||||
|
||||
connPool := newConnPool(opt)
|
||||
var connPool *pool.ConnPool
|
||||
|
||||
rdb := &Client{
|
||||
baseClient: &baseClient{
|
||||
opt: opt,
|
||||
},
|
||||
}
|
||||
rdb.init()
|
||||
|
||||
connPool = newConnPool(opt, rdb.dialHook)
|
||||
rdb.connPool = connPool
|
||||
rdb.onClose = failover.Close
|
||||
|
||||
failover.mu.Lock()
|
||||
failover.onFailover = func(ctx context.Context, addr string) {
|
||||
@@ -207,25 +241,18 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
|
||||
}
|
||||
failover.mu.Unlock()
|
||||
|
||||
c := Client{
|
||||
baseClient: newBaseClient(opt, connPool),
|
||||
ctx: context.Background(),
|
||||
}
|
||||
c.cmdable = c.Process
|
||||
c.onClose = failover.Close
|
||||
|
||||
return &c
|
||||
return rdb
|
||||
}
|
||||
|
||||
func masterSlaveDialer(
|
||||
func masterReplicaDialer(
|
||||
failover *sentinelFailover,
|
||||
) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
var addr string
|
||||
var err error
|
||||
|
||||
if failover.opt.SlaveOnly {
|
||||
addr, err = failover.RandomSlaveAddr(ctx)
|
||||
if failover.opt.ReplicaOnly {
|
||||
addr, err = failover.RandomReplicaAddr(ctx)
|
||||
} else {
|
||||
addr, err = failover.MasterAddr(ctx)
|
||||
if err == nil {
|
||||
@@ -255,37 +282,30 @@ func masterSlaveDialer(
|
||||
// SentinelClient is a client for a Redis Sentinel.
|
||||
type SentinelClient struct {
|
||||
*baseClient
|
||||
hooks
|
||||
ctx context.Context
|
||||
hooksMixin
|
||||
}
|
||||
|
||||
func NewSentinelClient(opt *Options) *SentinelClient {
|
||||
opt.init()
|
||||
c := &SentinelClient{
|
||||
baseClient: &baseClient{
|
||||
opt: opt,
|
||||
connPool: newConnPool(opt),
|
||||
opt: opt,
|
||||
},
|
||||
ctx: context.Background(),
|
||||
}
|
||||
|
||||
c.initHooks(hooks{
|
||||
dial: c.baseClient.dial,
|
||||
process: c.baseClient.process,
|
||||
})
|
||||
c.connPool = newConnPool(opt, c.dialHook)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *SentinelClient) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *SentinelClient) WithContext(ctx context.Context) *SentinelClient {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
clone := *c
|
||||
clone.ctx = ctx
|
||||
return &clone
|
||||
}
|
||||
|
||||
func (c *SentinelClient) Process(ctx context.Context, cmd Cmder) error {
|
||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
||||
err := c.processHook(ctx, cmd)
|
||||
cmd.SetErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *SentinelClient) pubSub() *PubSub {
|
||||
@@ -335,8 +355,8 @@ func (c *SentinelClient) GetMasterAddrByName(ctx context.Context, name string) *
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c *SentinelClient) Sentinels(ctx context.Context, name string) *SliceCmd {
|
||||
cmd := NewSliceCmd(ctx, "sentinel", "sentinels", name)
|
||||
func (c *SentinelClient) Sentinels(ctx context.Context, name string) *MapStringStringSliceCmd {
|
||||
cmd := NewMapStringStringSliceCmd(ctx, "sentinel", "sentinels", name)
|
||||
_ = c.Process(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
@@ -351,7 +371,7 @@ func (c *SentinelClient) Failover(ctx context.Context, name string) *StatusCmd {
|
||||
|
||||
// Reset resets all the masters with matching name. The pattern argument is a
|
||||
// glob-style pattern. The reset process clears any previous state in a master
|
||||
// (including a failover in progress), and removes every slave and sentinel
|
||||
// (including a failover in progress), and removes every replica and sentinel
|
||||
// already discovered and associated with the master.
|
||||
func (c *SentinelClient) Reset(ctx context.Context, pattern string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "sentinel", "reset", pattern)
|
||||
@@ -368,8 +388,8 @@ func (c *SentinelClient) FlushConfig(ctx context.Context) *StatusCmd {
|
||||
}
|
||||
|
||||
// Master shows the state and info of the specified master.
|
||||
func (c *SentinelClient) Master(ctx context.Context, name string) *StringStringMapCmd {
|
||||
cmd := NewStringStringMapCmd(ctx, "sentinel", "master", name)
|
||||
func (c *SentinelClient) Master(ctx context.Context, name string) *MapStringStringCmd {
|
||||
cmd := NewMapStringStringCmd(ctx, "sentinel", "master", name)
|
||||
_ = c.Process(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
@@ -381,9 +401,9 @@ func (c *SentinelClient) Masters(ctx context.Context) *SliceCmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Slaves shows a list of slaves for the specified master and their state.
|
||||
func (c *SentinelClient) Slaves(ctx context.Context, name string) *SliceCmd {
|
||||
cmd := NewSliceCmd(ctx, "sentinel", "slaves", name)
|
||||
// Replicas shows a list of replicas for the specified master and their state.
|
||||
func (c *SentinelClient) Replicas(ctx context.Context, name string) *MapStringStringSliceCmd {
|
||||
cmd := NewMapStringStringSliceCmd(ctx, "sentinel", "replicas", name)
|
||||
_ = c.Process(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
@@ -460,18 +480,18 @@ func (c *sentinelFailover) closeSentinel() error {
|
||||
return firstErr
|
||||
}
|
||||
|
||||
func (c *sentinelFailover) RandomSlaveAddr(ctx context.Context) (string, error) {
|
||||
func (c *sentinelFailover) RandomReplicaAddr(ctx context.Context) (string, error) {
|
||||
if c.opt == nil {
|
||||
return "", errors.New("opt is nil")
|
||||
}
|
||||
|
||||
addresses, err := c.slaveAddrs(ctx, false)
|
||||
addresses, err := c.replicaAddrs(ctx, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(addresses) == 0 && c.opt.UseDisconnectedSlaves {
|
||||
addresses, err = c.slaveAddrs(ctx, true)
|
||||
if len(addresses) == 0 && c.opt.UseDisconnectedReplicas {
|
||||
addresses, err = c.replicaAddrs(ctx, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -489,8 +509,15 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||
c.mu.RUnlock()
|
||||
|
||||
if sentinel != nil {
|
||||
addr := c.getMasterAddr(ctx, sentinel)
|
||||
if addr != "" {
|
||||
addr, err := c.getMasterAddr(ctx, sentinel)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return "", err
|
||||
}
|
||||
// Continue on other errors
|
||||
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName name=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
} else {
|
||||
return addr, nil
|
||||
}
|
||||
}
|
||||
@@ -499,11 +526,18 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.sentinel != nil {
|
||||
addr := c.getMasterAddr(ctx, c.sentinel)
|
||||
if addr != "" {
|
||||
addr, err := c.getMasterAddr(ctx, c.sentinel)
|
||||
if err != nil {
|
||||
_ = c.closeSentinel()
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return "", err
|
||||
}
|
||||
// Continue on other errors
|
||||
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName name=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
} else {
|
||||
return addr, nil
|
||||
}
|
||||
_ = c.closeSentinel()
|
||||
}
|
||||
|
||||
for i, sentinelAddr := range c.sentinelAddrs {
|
||||
@@ -511,9 +545,12 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||
|
||||
masterAddr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
|
||||
if err != nil {
|
||||
_ = sentinel.Close()
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return "", err
|
||||
}
|
||||
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName master=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
_ = sentinel.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -528,14 +565,21 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||
return "", errors.New("redis: all sentinels specified in configuration are unreachable")
|
||||
}
|
||||
|
||||
func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {
|
||||
func (c *sentinelFailover) replicaAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {
|
||||
c.mu.RLock()
|
||||
sentinel := c.sentinel
|
||||
c.mu.RUnlock()
|
||||
|
||||
if sentinel != nil {
|
||||
addrs := c.getSlaveAddrs(ctx, sentinel)
|
||||
if len(addrs) > 0 {
|
||||
addrs, err := c.getReplicaAddrs(ctx, sentinel)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
// Continue on other errors
|
||||
internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
} else if len(addrs) > 0 {
|
||||
return addrs, nil
|
||||
}
|
||||
}
|
||||
@@ -544,11 +588,21 @@ func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool)
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.sentinel != nil {
|
||||
addrs := c.getSlaveAddrs(ctx, c.sentinel)
|
||||
if len(addrs) > 0 {
|
||||
addrs, err := c.getReplicaAddrs(ctx, c.sentinel)
|
||||
if err != nil {
|
||||
_ = c.closeSentinel()
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
// Continue on other errors
|
||||
internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
} else if len(addrs) > 0 {
|
||||
return addrs, nil
|
||||
} else {
|
||||
// No error and no replicas.
|
||||
_ = c.closeSentinel()
|
||||
}
|
||||
_ = c.closeSentinel()
|
||||
}
|
||||
|
||||
var sentinelReachable bool
|
||||
@@ -556,15 +610,18 @@ func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool)
|
||||
for i, sentinelAddr := range c.sentinelAddrs {
|
||||
sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))
|
||||
|
||||
slaves, err := sentinel.Slaves(ctx, c.opt.MasterName).Result()
|
||||
replicas, err := sentinel.Replicas(ctx, c.opt.MasterName).Result()
|
||||
if err != nil {
|
||||
internal.Logger.Printf(ctx, "sentinel: Slaves master=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
_ = sentinel.Close()
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return nil, err
|
||||
}
|
||||
internal.Logger.Printf(ctx, "sentinel: Replicas master=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
continue
|
||||
}
|
||||
sentinelReachable = true
|
||||
addrs := parseSlaveAddrs(slaves, useDisconnected)
|
||||
addrs := parseReplicaAddrs(replicas, useDisconnected)
|
||||
if len(addrs) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -581,60 +638,42 @@ func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool)
|
||||
return []string{}, errors.New("redis: all sentinels specified in configuration are unreachable")
|
||||
}
|
||||
|
||||
func (c *sentinelFailover) getMasterAddr(ctx context.Context, sentinel *SentinelClient) string {
|
||||
func (c *sentinelFailover) getMasterAddr(ctx context.Context, sentinel *SentinelClient) (string, error) {
|
||||
addr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
|
||||
if err != nil {
|
||||
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName name=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
return ""
|
||||
return "", err
|
||||
}
|
||||
return net.JoinHostPort(addr[0], addr[1])
|
||||
return net.JoinHostPort(addr[0], addr[1]), nil
|
||||
}
|
||||
|
||||
func (c *sentinelFailover) getSlaveAddrs(ctx context.Context, sentinel *SentinelClient) []string {
|
||||
addrs, err := sentinel.Slaves(ctx, c.opt.MasterName).Result()
|
||||
func (c *sentinelFailover) getReplicaAddrs(ctx context.Context, sentinel *SentinelClient) ([]string, error) {
|
||||
addrs, err := sentinel.Replicas(ctx, c.opt.MasterName).Result()
|
||||
if err != nil {
|
||||
internal.Logger.Printf(ctx, "sentinel: Slaves name=%q failed: %s",
|
||||
internal.Logger.Printf(ctx, "sentinel: Replicas name=%q failed: %s",
|
||||
c.opt.MasterName, err)
|
||||
return []string{}
|
||||
return nil, err
|
||||
}
|
||||
return parseSlaveAddrs(addrs, false)
|
||||
return parseReplicaAddrs(addrs, false), nil
|
||||
}
|
||||
|
||||
func parseSlaveAddrs(addrs []interface{}, keepDisconnected bool) []string {
|
||||
func parseReplicaAddrs(addrs []map[string]string, keepDisconnected bool) []string {
|
||||
nodes := make([]string, 0, len(addrs))
|
||||
for _, node := range addrs {
|
||||
ip := ""
|
||||
port := ""
|
||||
flags := []string{}
|
||||
lastkey := ""
|
||||
isDown := false
|
||||
|
||||
for _, key := range node.([]interface{}) {
|
||||
switch lastkey {
|
||||
case "ip":
|
||||
ip = key.(string)
|
||||
case "port":
|
||||
port = key.(string)
|
||||
case "flags":
|
||||
flags = strings.Split(key.(string), ",")
|
||||
}
|
||||
lastkey = key.(string)
|
||||
}
|
||||
|
||||
for _, flag := range flags {
|
||||
switch flag {
|
||||
case "s_down", "o_down":
|
||||
isDown = true
|
||||
case "disconnected":
|
||||
if !keepDisconnected {
|
||||
if flags, ok := node["flags"]; ok {
|
||||
for _, flag := range strings.Split(flags, ",") {
|
||||
switch flag {
|
||||
case "s_down", "o_down":
|
||||
isDown = true
|
||||
case "disconnected":
|
||||
if !keepDisconnected {
|
||||
isDown = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isDown {
|
||||
nodes = append(nodes, net.JoinHostPort(ip, port))
|
||||
if !isDown && node["ip"] != "" && node["port"] != "" {
|
||||
nodes = append(nodes, net.JoinHostPort(node["ip"], node["port"]))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -672,7 +711,7 @@ func (c *sentinelFailover) setSentinel(ctx context.Context, sentinel *SentinelCl
|
||||
c.sentinel = sentinel
|
||||
c.discoverSentinels(ctx)
|
||||
|
||||
c.pubsub = sentinel.Subscribe(ctx, "+switch-master", "+slave-reconf-done")
|
||||
c.pubsub = sentinel.Subscribe(ctx, "+switch-master", "+replica-reconf-done")
|
||||
go c.listen(c.pubsub)
|
||||
}
|
||||
|
||||
@@ -683,16 +722,13 @@ func (c *sentinelFailover) discoverSentinels(ctx context.Context) {
|
||||
return
|
||||
}
|
||||
for _, sentinel := range sentinels {
|
||||
vals := sentinel.([]interface{})
|
||||
var ip, port string
|
||||
for i := 0; i < len(vals); i += 2 {
|
||||
key := vals[i].(string)
|
||||
switch key {
|
||||
case "ip":
|
||||
ip = vals[i+1].(string)
|
||||
case "port":
|
||||
port = vals[i+1].(string)
|
||||
}
|
||||
ip, ok := sentinel["ip"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
port, ok := sentinel["port"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if ip != "" && port != "" {
|
||||
sentinelAddr := net.JoinHostPort(ip, port)
|
||||
@@ -742,7 +778,7 @@ func contains(slice []string, str string) bool {
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// NewFailoverClusterClient returns a client that supports routing read-only commands
|
||||
// to a slave node.
|
||||
// to a replica node.
|
||||
func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
|
||||
sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
|
||||
copy(sentinelAddrs, failoverOpt.SentinelAddrs)
|
||||
@@ -763,14 +799,14 @@ func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
|
||||
Addr: masterAddr,
|
||||
}}
|
||||
|
||||
slaveAddrs, err := failover.slaveAddrs(ctx, false)
|
||||
replicaAddrs, err := failover.replicaAddrs(ctx, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, slaveAddr := range slaveAddrs {
|
||||
for _, replicaAddr := range replicaAddrs {
|
||||
nodes = append(nodes, ClusterNode{
|
||||
Addr: slaveAddr,
|
||||
Addr: replicaAddr,
|
||||
})
|
||||
}
|
||||
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package redis
|
||||
|
||||
import "context"
|
||||
|
||||
type SetCmdable interface {
|
||||
SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd
|
||||
SCard(ctx context.Context, key string) *IntCmd
|
||||
SDiff(ctx context.Context, keys ...string) *StringSliceCmd
|
||||
SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd
|
||||
SInter(ctx context.Context, keys ...string) *StringSliceCmd
|
||||
SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd
|
||||
SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd
|
||||
SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd
|
||||
SMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd
|
||||
SMembers(ctx context.Context, key string) *StringSliceCmd
|
||||
SMembersMap(ctx context.Context, key string) *StringStructMapCmd
|
||||
SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd
|
||||
SPop(ctx context.Context, key string) *StringCmd
|
||||
SPopN(ctx context.Context, key string, count int64) *StringSliceCmd
|
||||
SRandMember(ctx context.Context, key string) *StringCmd
|
||||
SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd
|
||||
SRem(ctx context.Context, key string, members ...interface{}) *IntCmd
|
||||
SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
|
||||
SUnion(ctx context.Context, keys ...string) *StringSliceCmd
|
||||
SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
func (c cmdable) SAdd(ctx context.Context, key string, members ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(members))
|
||||
args[0] = "sadd"
|
||||
args[1] = key
|
||||
args = appendArgs(args, members)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SCard(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "scard", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SDiff(ctx context.Context, keys ...string) *StringSliceCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "sdiff"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 2+len(keys))
|
||||
args[0] = "sdiffstore"
|
||||
args[1] = destination
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SInter(ctx context.Context, keys ...string) *StringSliceCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "sinter"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 4+len(keys))
|
||||
args[0] = "sintercard"
|
||||
numkeys := int64(0)
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
numkeys++
|
||||
}
|
||||
args[1] = numkeys
|
||||
args[2+numkeys] = "limit"
|
||||
args[3+numkeys] = limit
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SInterStore(ctx context.Context, destination string, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 2+len(keys))
|
||||
args[0] = "sinterstore"
|
||||
args[1] = destination
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SIsMember(ctx context.Context, key string, member interface{}) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "sismember", key, member)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SMIsMember Redis `SMISMEMBER key member [member ...]` command.
|
||||
func (c cmdable) SMIsMember(ctx context.Context, key string, members ...interface{}) *BoolSliceCmd {
|
||||
args := make([]interface{}, 2, 2+len(members))
|
||||
args[0] = "smismember"
|
||||
args[1] = key
|
||||
args = appendArgs(args, members)
|
||||
cmd := NewBoolSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SMembers Redis `SMEMBERS key` command output as a slice.
|
||||
func (c cmdable) SMembers(ctx context.Context, key string) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "smembers", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SMembersMap Redis `SMEMBERS key` command output as a map.
|
||||
func (c cmdable) SMembersMap(ctx context.Context, key string) *StringStructMapCmd {
|
||||
cmd := NewStringStructMapCmd(ctx, "smembers", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SMove(ctx context.Context, source, destination string, member interface{}) *BoolCmd {
|
||||
cmd := NewBoolCmd(ctx, "smove", source, destination, member)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SPop Redis `SPOP key` command.
|
||||
func (c cmdable) SPop(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "spop", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SPopN Redis `SPOP key count` command.
|
||||
func (c cmdable) SPopN(ctx context.Context, key string, count int64) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "spop", key, count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SRandMember Redis `SRANDMEMBER key` command.
|
||||
func (c cmdable) SRandMember(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "srandmember", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SRandMemberN Redis `SRANDMEMBER key count` command.
|
||||
func (c cmdable) SRandMemberN(ctx context.Context, key string, count int64) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "srandmember", key, count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SRem(ctx context.Context, key string, members ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(members))
|
||||
args[0] = "srem"
|
||||
args[1] = key
|
||||
args = appendArgs(args, members)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SUnion(ctx context.Context, keys ...string) *StringSliceCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "sunion"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SUnionStore(ctx context.Context, destination string, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 2+len(keys))
|
||||
args[0] = "sunionstore"
|
||||
args[1] = destination
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
|
||||
args := []interface{}{"sscan", key, cursor}
|
||||
if match != "" {
|
||||
args = append(args, "match", match)
|
||||
}
|
||||
if count > 0 {
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+772
@@ -0,0 +1,772 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SortedSetCmdable interface {
|
||||
BZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd
|
||||
BZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd
|
||||
BZMPop(ctx context.Context, timeout time.Duration, order string, count int64, keys ...string) *ZSliceWithKeyCmd
|
||||
ZAdd(ctx context.Context, key string, members ...Z) *IntCmd
|
||||
ZAddLT(ctx context.Context, key string, members ...Z) *IntCmd
|
||||
ZAddGT(ctx context.Context, key string, members ...Z) *IntCmd
|
||||
ZAddNX(ctx context.Context, key string, members ...Z) *IntCmd
|
||||
ZAddXX(ctx context.Context, key string, members ...Z) *IntCmd
|
||||
ZAddArgs(ctx context.Context, key string, args ZAddArgs) *IntCmd
|
||||
ZAddArgsIncr(ctx context.Context, key string, args ZAddArgs) *FloatCmd
|
||||
ZCard(ctx context.Context, key string) *IntCmd
|
||||
ZCount(ctx context.Context, key, min, max string) *IntCmd
|
||||
ZLexCount(ctx context.Context, key, min, max string) *IntCmd
|
||||
ZIncrBy(ctx context.Context, key string, increment float64, member string) *FloatCmd
|
||||
ZInter(ctx context.Context, store *ZStore) *StringSliceCmd
|
||||
ZInterWithScores(ctx context.Context, store *ZStore) *ZSliceCmd
|
||||
ZInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd
|
||||
ZInterStore(ctx context.Context, destination string, store *ZStore) *IntCmd
|
||||
ZMPop(ctx context.Context, order string, count int64, keys ...string) *ZSliceWithKeyCmd
|
||||
ZMScore(ctx context.Context, key string, members ...string) *FloatSliceCmd
|
||||
ZPopMax(ctx context.Context, key string, count ...int64) *ZSliceCmd
|
||||
ZPopMin(ctx context.Context, key string, count ...int64) *ZSliceCmd
|
||||
ZRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd
|
||||
ZRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd
|
||||
ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd
|
||||
ZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd
|
||||
ZRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd
|
||||
ZRangeArgs(ctx context.Context, z ZRangeArgs) *StringSliceCmd
|
||||
ZRangeArgsWithScores(ctx context.Context, z ZRangeArgs) *ZSliceCmd
|
||||
ZRangeStore(ctx context.Context, dst string, z ZRangeArgs) *IntCmd
|
||||
ZRank(ctx context.Context, key, member string) *IntCmd
|
||||
ZRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd
|
||||
ZRem(ctx context.Context, key string, members ...interface{}) *IntCmd
|
||||
ZRemRangeByRank(ctx context.Context, key string, start, stop int64) *IntCmd
|
||||
ZRemRangeByScore(ctx context.Context, key, min, max string) *IntCmd
|
||||
ZRemRangeByLex(ctx context.Context, key, min, max string) *IntCmd
|
||||
ZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd
|
||||
ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd
|
||||
ZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd
|
||||
ZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd
|
||||
ZRevRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd
|
||||
ZRevRank(ctx context.Context, key, member string) *IntCmd
|
||||
ZRevRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd
|
||||
ZScore(ctx context.Context, key, member string) *FloatCmd
|
||||
ZUnionStore(ctx context.Context, dest string, store *ZStore) *IntCmd
|
||||
ZRandMember(ctx context.Context, key string, count int) *StringSliceCmd
|
||||
ZRandMemberWithScores(ctx context.Context, key string, count int) *ZSliceCmd
|
||||
ZUnion(ctx context.Context, store ZStore) *StringSliceCmd
|
||||
ZUnionWithScores(ctx context.Context, store ZStore) *ZSliceCmd
|
||||
ZDiff(ctx context.Context, keys ...string) *StringSliceCmd
|
||||
ZDiffWithScores(ctx context.Context, keys ...string) *ZSliceCmd
|
||||
ZDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd
|
||||
ZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd
|
||||
}
|
||||
|
||||
// BZPopMax Redis `BZPOPMAX key [key ...] timeout` command.
|
||||
func (c cmdable) BZPopMax(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd {
|
||||
args := make([]interface{}, 1+len(keys)+1)
|
||||
args[0] = "bzpopmax"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
args[len(args)-1] = formatSec(ctx, timeout)
|
||||
cmd := NewZWithKeyCmd(ctx, args...)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// BZPopMin Redis `BZPOPMIN key [key ...] timeout` command.
|
||||
func (c cmdable) BZPopMin(ctx context.Context, timeout time.Duration, keys ...string) *ZWithKeyCmd {
|
||||
args := make([]interface{}, 1+len(keys)+1)
|
||||
args[0] = "bzpopmin"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
args[len(args)-1] = formatSec(ctx, timeout)
|
||||
cmd := NewZWithKeyCmd(ctx, args...)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// BZMPop is the blocking variant of ZMPOP.
|
||||
// When any of the sorted sets contains elements, this command behaves exactly like ZMPOP.
|
||||
// When all sorted sets are empty, Redis will block the connection until another client adds members to one of the keys or until the timeout elapses.
|
||||
// A timeout of zero can be used to block indefinitely.
|
||||
// example: client.BZMPop(ctx, 0,"max", 1, "set")
|
||||
func (c cmdable) BZMPop(ctx context.Context, timeout time.Duration, order string, count int64, keys ...string) *ZSliceWithKeyCmd {
|
||||
args := make([]interface{}, 3+len(keys), 6+len(keys))
|
||||
args[0] = "bzmpop"
|
||||
args[1] = formatSec(ctx, timeout)
|
||||
args[2] = len(keys)
|
||||
for i, key := range keys {
|
||||
args[3+i] = key
|
||||
}
|
||||
args = append(args, strings.ToLower(order), "count", count)
|
||||
cmd := NewZSliceWithKeyCmd(ctx, args...)
|
||||
cmd.setReadTimeout(timeout)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZAddArgs WARN: The GT, LT and NX options are mutually exclusive.
|
||||
type ZAddArgs struct {
|
||||
NX bool
|
||||
XX bool
|
||||
LT bool
|
||||
GT bool
|
||||
Ch bool
|
||||
Members []Z
|
||||
}
|
||||
|
||||
func (c cmdable) zAddArgs(key string, args ZAddArgs, incr bool) []interface{} {
|
||||
a := make([]interface{}, 0, 6+2*len(args.Members))
|
||||
a = append(a, "zadd", key)
|
||||
|
||||
// The GT, LT and NX options are mutually exclusive.
|
||||
if args.NX {
|
||||
a = append(a, "nx")
|
||||
} else {
|
||||
if args.XX {
|
||||
a = append(a, "xx")
|
||||
}
|
||||
if args.GT {
|
||||
a = append(a, "gt")
|
||||
} else if args.LT {
|
||||
a = append(a, "lt")
|
||||
}
|
||||
}
|
||||
if args.Ch {
|
||||
a = append(a, "ch")
|
||||
}
|
||||
if incr {
|
||||
a = append(a, "incr")
|
||||
}
|
||||
for _, m := range args.Members {
|
||||
a = append(a, m.Score)
|
||||
a = append(a, m.Member)
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (c cmdable) ZAddArgs(ctx context.Context, key string, args ZAddArgs) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, c.zAddArgs(key, args, false)...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZAddArgsIncr(ctx context.Context, key string, args ZAddArgs) *FloatCmd {
|
||||
cmd := NewFloatCmd(ctx, c.zAddArgs(key, args, true)...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZAdd Redis `ZADD key score member [score member ...]` command.
|
||||
func (c cmdable) ZAdd(ctx context.Context, key string, members ...Z) *IntCmd {
|
||||
return c.ZAddArgs(ctx, key, ZAddArgs{
|
||||
Members: members,
|
||||
})
|
||||
}
|
||||
|
||||
// ZAddLT Redis `ZADD key LT score member [score member ...]` command.
|
||||
func (c cmdable) ZAddLT(ctx context.Context, key string, members ...Z) *IntCmd {
|
||||
return c.ZAddArgs(ctx, key, ZAddArgs{
|
||||
LT: true,
|
||||
Members: members,
|
||||
})
|
||||
}
|
||||
|
||||
// ZAddGT Redis `ZADD key GT score member [score member ...]` command.
|
||||
func (c cmdable) ZAddGT(ctx context.Context, key string, members ...Z) *IntCmd {
|
||||
return c.ZAddArgs(ctx, key, ZAddArgs{
|
||||
GT: true,
|
||||
Members: members,
|
||||
})
|
||||
}
|
||||
|
||||
// ZAddNX Redis `ZADD key NX score member [score member ...]` command.
|
||||
func (c cmdable) ZAddNX(ctx context.Context, key string, members ...Z) *IntCmd {
|
||||
return c.ZAddArgs(ctx, key, ZAddArgs{
|
||||
NX: true,
|
||||
Members: members,
|
||||
})
|
||||
}
|
||||
|
||||
// ZAddXX Redis `ZADD key XX score member [score member ...]` command.
|
||||
func (c cmdable) ZAddXX(ctx context.Context, key string, members ...Z) *IntCmd {
|
||||
return c.ZAddArgs(ctx, key, ZAddArgs{
|
||||
XX: true,
|
||||
Members: members,
|
||||
})
|
||||
}
|
||||
|
||||
func (c cmdable) ZCard(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "zcard", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZCount(ctx context.Context, key, min, max string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "zcount", key, min, max)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZLexCount(ctx context.Context, key, min, max string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "zlexcount", key, min, max)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZIncrBy(ctx context.Context, key string, increment float64, member string) *FloatCmd {
|
||||
cmd := NewFloatCmd(ctx, "zincrby", key, increment, member)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZInterStore(ctx context.Context, destination string, store *ZStore) *IntCmd {
|
||||
args := make([]interface{}, 0, 3+store.len())
|
||||
args = append(args, "zinterstore", destination, len(store.Keys))
|
||||
args = store.appendArgs(args)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(3)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZInter(ctx context.Context, store *ZStore) *StringSliceCmd {
|
||||
args := make([]interface{}, 0, 2+store.len())
|
||||
args = append(args, "zinter", len(store.Keys))
|
||||
args = store.appendArgs(args)
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZInterWithScores(ctx context.Context, store *ZStore) *ZSliceCmd {
|
||||
args := make([]interface{}, 0, 3+store.len())
|
||||
args = append(args, "zinter", len(store.Keys))
|
||||
args = store.appendArgs(args)
|
||||
args = append(args, "withscores")
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZInterCard(ctx context.Context, limit int64, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 4+len(keys))
|
||||
args[0] = "zintercard"
|
||||
numkeys := int64(0)
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
numkeys++
|
||||
}
|
||||
args[1] = numkeys
|
||||
args[2+numkeys] = "limit"
|
||||
args[3+numkeys] = limit
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZMPop Pops one or more elements with the highest or lowest score from the first non-empty sorted set key from the list of provided key names.
|
||||
// direction: "max" (highest score) or "min" (lowest score), count: > 0
|
||||
// example: client.ZMPop(ctx, "max", 5, "set1", "set2")
|
||||
func (c cmdable) ZMPop(ctx context.Context, order string, count int64, keys ...string) *ZSliceWithKeyCmd {
|
||||
args := make([]interface{}, 2+len(keys), 5+len(keys))
|
||||
args[0] = "zmpop"
|
||||
args[1] = len(keys)
|
||||
for i, key := range keys {
|
||||
args[2+i] = key
|
||||
}
|
||||
args = append(args, strings.ToLower(order), "count", count)
|
||||
cmd := NewZSliceWithKeyCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZMScore(ctx context.Context, key string, members ...string) *FloatSliceCmd {
|
||||
args := make([]interface{}, 2+len(members))
|
||||
args[0] = "zmscore"
|
||||
args[1] = key
|
||||
for i, member := range members {
|
||||
args[2+i] = member
|
||||
}
|
||||
cmd := NewFloatSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZPopMax(ctx context.Context, key string, count ...int64) *ZSliceCmd {
|
||||
args := []interface{}{
|
||||
"zpopmax",
|
||||
key,
|
||||
}
|
||||
|
||||
switch len(count) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
args = append(args, count[0])
|
||||
default:
|
||||
panic("too many arguments")
|
||||
}
|
||||
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZPopMin(ctx context.Context, key string, count ...int64) *ZSliceCmd {
|
||||
args := []interface{}{
|
||||
"zpopmin",
|
||||
key,
|
||||
}
|
||||
|
||||
switch len(count) {
|
||||
case 0:
|
||||
break
|
||||
case 1:
|
||||
args = append(args, count[0])
|
||||
default:
|
||||
panic("too many arguments")
|
||||
}
|
||||
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZRangeArgs is all the options of the ZRange command.
|
||||
// In version> 6.2.0, you can replace the(cmd):
|
||||
//
|
||||
// ZREVRANGE,
|
||||
// ZRANGEBYSCORE,
|
||||
// ZREVRANGEBYSCORE,
|
||||
// ZRANGEBYLEX,
|
||||
// ZREVRANGEBYLEX.
|
||||
//
|
||||
// Please pay attention to your redis-server version.
|
||||
//
|
||||
// Rev, ByScore, ByLex and Offset+Count options require redis-server 6.2.0 and higher.
|
||||
type ZRangeArgs struct {
|
||||
Key string
|
||||
|
||||
// When the ByScore option is provided, the open interval(exclusive) can be set.
|
||||
// By default, the score intervals specified by <Start> and <Stop> are closed (inclusive).
|
||||
// It is similar to the deprecated(6.2.0+) ZRangeByScore command.
|
||||
// For example:
|
||||
// ZRangeArgs{
|
||||
// Key: "example-key",
|
||||
// Start: "(3",
|
||||
// Stop: 8,
|
||||
// ByScore: true,
|
||||
// }
|
||||
// cmd: "ZRange example-key (3 8 ByScore" (3 < score <= 8).
|
||||
//
|
||||
// For the ByLex option, it is similar to the deprecated(6.2.0+) ZRangeByLex command.
|
||||
// You can set the <Start> and <Stop> options as follows:
|
||||
// ZRangeArgs{
|
||||
// Key: "example-key",
|
||||
// Start: "[abc",
|
||||
// Stop: "(def",
|
||||
// ByLex: true,
|
||||
// }
|
||||
// cmd: "ZRange example-key [abc (def ByLex"
|
||||
//
|
||||
// For normal cases (ByScore==false && ByLex==false), <Start> and <Stop> should be set to the index range (int).
|
||||
// You can read the documentation for more information: https://redis.io/commands/zrange
|
||||
Start interface{}
|
||||
Stop interface{}
|
||||
|
||||
// The ByScore and ByLex options are mutually exclusive.
|
||||
ByScore bool
|
||||
ByLex bool
|
||||
|
||||
Rev bool
|
||||
|
||||
// limit offset count.
|
||||
Offset int64
|
||||
Count int64
|
||||
}
|
||||
|
||||
func (z ZRangeArgs) appendArgs(args []interface{}) []interface{} {
|
||||
// For Rev+ByScore/ByLex, we need to adjust the position of <Start> and <Stop>.
|
||||
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)
|
||||
}
|
||||
|
||||
if z.ByScore {
|
||||
args = append(args, "byscore")
|
||||
} else if z.ByLex {
|
||||
args = append(args, "bylex")
|
||||
}
|
||||
if z.Rev {
|
||||
args = append(args, "rev")
|
||||
}
|
||||
if z.Offset != 0 || z.Count != 0 {
|
||||
args = append(args, "limit", z.Offset, z.Count)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func (c cmdable) ZRangeArgs(ctx context.Context, z ZRangeArgs) *StringSliceCmd {
|
||||
args := make([]interface{}, 0, 9)
|
||||
args = append(args, "zrange")
|
||||
args = z.appendArgs(args)
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRangeArgsWithScores(ctx context.Context, z ZRangeArgs) *ZSliceCmd {
|
||||
args := make([]interface{}, 0, 10)
|
||||
args = append(args, "zrange")
|
||||
args = z.appendArgs(args)
|
||||
args = append(args, "withscores")
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd {
|
||||
return c.ZRangeArgs(ctx, ZRangeArgs{
|
||||
Key: key,
|
||||
Start: start,
|
||||
Stop: stop,
|
||||
})
|
||||
}
|
||||
|
||||
func (c cmdable) ZRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd {
|
||||
return c.ZRangeArgsWithScores(ctx, ZRangeArgs{
|
||||
Key: key,
|
||||
Start: start,
|
||||
Stop: stop,
|
||||
})
|
||||
}
|
||||
|
||||
type ZRangeBy struct {
|
||||
Min, Max string
|
||||
Offset, Count int64
|
||||
}
|
||||
|
||||
func (c cmdable) zRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy, withScores bool) *StringSliceCmd {
|
||||
args := []interface{}{zcmd, key, opt.Min, opt.Max}
|
||||
if withScores {
|
||||
args = append(args, "withscores")
|
||||
}
|
||||
if opt.Offset != 0 || opt.Count != 0 {
|
||||
args = append(
|
||||
args,
|
||||
"limit",
|
||||
opt.Offset,
|
||||
opt.Count,
|
||||
)
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
|
||||
return c.zRangeBy(ctx, "zrangebyscore", key, opt, false)
|
||||
}
|
||||
|
||||
func (c cmdable) ZRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
|
||||
return c.zRangeBy(ctx, "zrangebylex", key, opt, false)
|
||||
}
|
||||
|
||||
func (c cmdable) ZRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd {
|
||||
args := []interface{}{"zrangebyscore", key, opt.Min, opt.Max, "withscores"}
|
||||
if opt.Offset != 0 || opt.Count != 0 {
|
||||
args = append(
|
||||
args,
|
||||
"limit",
|
||||
opt.Offset,
|
||||
opt.Count,
|
||||
)
|
||||
}
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRangeStore(ctx context.Context, dst string, z ZRangeArgs) *IntCmd {
|
||||
args := make([]interface{}, 0, 10)
|
||||
args = append(args, "zrangestore", dst)
|
||||
args = z.appendArgs(args)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRank(ctx context.Context, key, member string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "zrank", key, member)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZRankWithScore according to the Redis documentation, if member does not exist
|
||||
// in the sorted set or key does not exist, it will return a redis.Nil error.
|
||||
func (c cmdable) ZRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd {
|
||||
cmd := NewRankWithScoreCmd(ctx, "zrank", key, member, "withscore")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRem(ctx context.Context, key string, members ...interface{}) *IntCmd {
|
||||
args := make([]interface{}, 2, 2+len(members))
|
||||
args[0] = "zrem"
|
||||
args[1] = key
|
||||
args = appendArgs(args, members)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRemRangeByRank(ctx context.Context, key string, start, stop int64) *IntCmd {
|
||||
cmd := NewIntCmd(
|
||||
ctx,
|
||||
"zremrangebyrank",
|
||||
key,
|
||||
start,
|
||||
stop,
|
||||
)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRemRangeByScore(ctx context.Context, key, min, max string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "zremrangebyscore", key, min, max)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRemRangeByLex(ctx context.Context, key, min, max string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "zremrangebylex", key, min, max)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRevRange(ctx context.Context, key string, start, stop int64) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "zrevrange", key, start, stop)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZRevRangeWithScores according to the Redis documentation, if member does not exist
|
||||
// in the sorted set or key does not exist, it will return a redis.Nil error.
|
||||
func (c cmdable) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) *ZSliceCmd {
|
||||
cmd := NewZSliceCmd(ctx, "zrevrange", key, start, stop, "withscores")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) zRevRangeBy(ctx context.Context, zcmd, key string, opt *ZRangeBy) *StringSliceCmd {
|
||||
args := []interface{}{zcmd, key, opt.Max, opt.Min}
|
||||
if opt.Offset != 0 || opt.Count != 0 {
|
||||
args = append(
|
||||
args,
|
||||
"limit",
|
||||
opt.Offset,
|
||||
opt.Count,
|
||||
)
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRevRangeByScore(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
|
||||
return c.zRevRangeBy(ctx, "zrevrangebyscore", key, opt)
|
||||
}
|
||||
|
||||
func (c cmdable) ZRevRangeByLex(ctx context.Context, key string, opt *ZRangeBy) *StringSliceCmd {
|
||||
return c.zRevRangeBy(ctx, "zrevrangebylex", key, opt)
|
||||
}
|
||||
|
||||
func (c cmdable) ZRevRangeByScoreWithScores(ctx context.Context, key string, opt *ZRangeBy) *ZSliceCmd {
|
||||
args := []interface{}{"zrevrangebyscore", key, opt.Max, opt.Min, "withscores"}
|
||||
if opt.Offset != 0 || opt.Count != 0 {
|
||||
args = append(
|
||||
args,
|
||||
"limit",
|
||||
opt.Offset,
|
||||
opt.Count,
|
||||
)
|
||||
}
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRevRank(ctx context.Context, key, member string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "zrevrank", key, member)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZRevRankWithScore(ctx context.Context, key, member string) *RankWithScoreCmd {
|
||||
cmd := NewRankWithScoreCmd(ctx, "zrevrank", key, member, "withscore")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZScore(ctx context.Context, key, member string) *FloatCmd {
|
||||
cmd := NewFloatCmd(ctx, "zscore", key, member)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZUnion(ctx context.Context, store ZStore) *StringSliceCmd {
|
||||
args := make([]interface{}, 0, 2+store.len())
|
||||
args = append(args, "zunion", len(store.Keys))
|
||||
args = store.appendArgs(args)
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZUnionWithScores(ctx context.Context, store ZStore) *ZSliceCmd {
|
||||
args := make([]interface{}, 0, 3+store.len())
|
||||
args = append(args, "zunion", len(store.Keys))
|
||||
args = store.appendArgs(args)
|
||||
args = append(args, "withscores")
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZUnionStore(ctx context.Context, dest string, store *ZStore) *IntCmd {
|
||||
args := make([]interface{}, 0, 3+store.len())
|
||||
args = append(args, "zunionstore", dest, len(store.Keys))
|
||||
args = store.appendArgs(args)
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(3)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZRandMember redis-server version >= 6.2.0.
|
||||
func (c cmdable) ZRandMember(ctx context.Context, key string, count int) *StringSliceCmd {
|
||||
cmd := NewStringSliceCmd(ctx, "zrandmember", key, count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZRandMemberWithScores redis-server version >= 6.2.0.
|
||||
func (c cmdable) ZRandMemberWithScores(ctx context.Context, key string, count int) *ZSliceCmd {
|
||||
cmd := NewZSliceCmd(ctx, "zrandmember", key, count, "withscores")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZDiff redis-server version >= 6.2.0.
|
||||
func (c cmdable) ZDiff(ctx context.Context, keys ...string) *StringSliceCmd {
|
||||
args := make([]interface{}, 2+len(keys))
|
||||
args[0] = "zdiff"
|
||||
args[1] = len(keys)
|
||||
for i, key := range keys {
|
||||
args[i+2] = key
|
||||
}
|
||||
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZDiffWithScores redis-server version >= 6.2.0.
|
||||
func (c cmdable) ZDiffWithScores(ctx context.Context, keys ...string) *ZSliceCmd {
|
||||
args := make([]interface{}, 3+len(keys))
|
||||
args[0] = "zdiff"
|
||||
args[1] = len(keys)
|
||||
for i, key := range keys {
|
||||
args[i+2] = key
|
||||
}
|
||||
args[len(keys)+2] = "withscores"
|
||||
|
||||
cmd := NewZSliceCmd(ctx, args...)
|
||||
cmd.SetFirstKeyPos(2)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// ZDiffStore redis-server version >=6.2.0.
|
||||
func (c cmdable) ZDiffStore(ctx context.Context, destination string, keys ...string) *IntCmd {
|
||||
args := make([]interface{}, 0, 3+len(keys))
|
||||
args = append(args, "zdiffstore", destination, len(keys))
|
||||
for _, key := range keys {
|
||||
args = append(args, key)
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) ZScan(ctx context.Context, key string, cursor uint64, match string, count int64) *ScanCmd {
|
||||
args := []interface{}{"zscan", key, cursor}
|
||||
if match != "" {
|
||||
args = append(args, "match", match)
|
||||
}
|
||||
if count > 0 {
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
cmd := NewScanCmd(ctx, c, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Z represents sorted set member.
|
||||
type Z struct {
|
||||
Score float64
|
||||
Member interface{}
|
||||
}
|
||||
|
||||
// ZWithKey represents sorted set member including the name of the key where it was popped.
|
||||
type ZWithKey struct {
|
||||
Z
|
||||
Key string
|
||||
}
|
||||
|
||||
// ZStore is used as an arg to ZInter/ZInterStore and ZUnion/ZUnionStore.
|
||||
type ZStore struct {
|
||||
Keys []string
|
||||
Weights []float64
|
||||
// Can be SUM, MIN or MAX.
|
||||
Aggregate string
|
||||
}
|
||||
|
||||
func (z ZStore) len() (n int) {
|
||||
n = len(z.Keys)
|
||||
if len(z.Weights) > 0 {
|
||||
n += 1 + len(z.Weights)
|
||||
}
|
||||
if z.Aggregate != "" {
|
||||
n += 2
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (z ZStore) appendArgs(args []interface{}) []interface{} {
|
||||
for _, key := range z.Keys {
|
||||
args = append(args, key)
|
||||
}
|
||||
if len(z.Weights) > 0 {
|
||||
args = append(args, "weights")
|
||||
for _, weights := range z.Weights {
|
||||
args = append(args, weights)
|
||||
}
|
||||
}
|
||||
if z.Aggregate != "" {
|
||||
args = append(args, "aggregate", z.Aggregate)
|
||||
}
|
||||
return args
|
||||
}
|
||||
+438
@@ -0,0 +1,438 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StreamCmdable interface {
|
||||
XAdd(ctx context.Context, a *XAddArgs) *StringCmd
|
||||
XDel(ctx context.Context, stream string, ids ...string) *IntCmd
|
||||
XLen(ctx context.Context, stream string) *IntCmd
|
||||
XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd
|
||||
XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd
|
||||
XRevRange(ctx context.Context, stream string, start, stop string) *XMessageSliceCmd
|
||||
XRevRangeN(ctx context.Context, stream string, start, stop string, count int64) *XMessageSliceCmd
|
||||
XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd
|
||||
XReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd
|
||||
XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd
|
||||
XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd
|
||||
XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd
|
||||
XGroupDestroy(ctx context.Context, stream, group string) *IntCmd
|
||||
XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *IntCmd
|
||||
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
|
||||
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
|
||||
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
|
||||
XTrimMinID(ctx context.Context, key string, minID string) *IntCmd
|
||||
XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd
|
||||
XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd
|
||||
XInfoStream(ctx context.Context, key string) *XInfoStreamCmd
|
||||
XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd
|
||||
XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd
|
||||
}
|
||||
|
||||
// XAddArgs accepts values in the following formats:
|
||||
// - XAddArgs.Values = []interface{}{"key1", "value1", "key2", "value2"}
|
||||
// - XAddArgs.Values = []string("key1", "value1", "key2", "value2")
|
||||
// - XAddArgs.Values = map[string]interface{}{"key1": "value1", "key2": "value2"}
|
||||
//
|
||||
// Note that map will not preserve the order of key-value pairs.
|
||||
// MaxLen/MaxLenApprox and MinID are in conflict, only one of them can be used.
|
||||
type XAddArgs struct {
|
||||
Stream string
|
||||
NoMkStream bool
|
||||
MaxLen int64 // MAXLEN N
|
||||
MinID string
|
||||
// Approx causes MaxLen and MinID to use "~" matcher (instead of "=").
|
||||
Approx bool
|
||||
Limit int64
|
||||
ID string
|
||||
Values interface{}
|
||||
}
|
||||
|
||||
func (c cmdable) XAdd(ctx context.Context, a *XAddArgs) *StringCmd {
|
||||
args := make([]interface{}, 0, 11)
|
||||
args = append(args, "xadd", a.Stream)
|
||||
if a.NoMkStream {
|
||||
args = append(args, "nomkstream")
|
||||
}
|
||||
switch {
|
||||
case a.MaxLen > 0:
|
||||
if a.Approx {
|
||||
args = append(args, "maxlen", "~", a.MaxLen)
|
||||
} else {
|
||||
args = append(args, "maxlen", a.MaxLen)
|
||||
}
|
||||
case a.MinID != "":
|
||||
if a.Approx {
|
||||
args = append(args, "minid", "~", a.MinID)
|
||||
} else {
|
||||
args = append(args, "minid", a.MinID)
|
||||
}
|
||||
}
|
||||
if a.Limit > 0 {
|
||||
args = append(args, "limit", a.Limit)
|
||||
}
|
||||
if a.ID != "" {
|
||||
args = append(args, a.ID)
|
||||
} else {
|
||||
args = append(args, "*")
|
||||
}
|
||||
args = appendArg(args, a.Values)
|
||||
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XDel(ctx context.Context, stream string, ids ...string) *IntCmd {
|
||||
args := []interface{}{"xdel", stream}
|
||||
for _, id := range ids {
|
||||
args = append(args, id)
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XLen(ctx context.Context, stream string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "xlen", stream)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd {
|
||||
cmd := NewXMessageSliceCmd(ctx, "xrange", stream, start, stop)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd {
|
||||
cmd := NewXMessageSliceCmd(ctx, "xrange", stream, start, stop, "count", count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XRevRange(ctx context.Context, stream, start, stop string) *XMessageSliceCmd {
|
||||
cmd := NewXMessageSliceCmd(ctx, "xrevrange", stream, start, stop)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XRevRangeN(ctx context.Context, stream, start, stop string, count int64) *XMessageSliceCmd {
|
||||
cmd := NewXMessageSliceCmd(ctx, "xrevrange", stream, start, stop, "count", count)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type XReadArgs struct {
|
||||
Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2
|
||||
Count int64
|
||||
Block time.Duration
|
||||
}
|
||||
|
||||
func (c cmdable) XRead(ctx context.Context, a *XReadArgs) *XStreamSliceCmd {
|
||||
args := make([]interface{}, 0, 6+len(a.Streams))
|
||||
args = append(args, "xread")
|
||||
|
||||
keyPos := int8(1)
|
||||
if a.Count > 0 {
|
||||
args = append(args, "count")
|
||||
args = append(args, a.Count)
|
||||
keyPos += 2
|
||||
}
|
||||
if a.Block >= 0 {
|
||||
args = append(args, "block")
|
||||
args = append(args, int64(a.Block/time.Millisecond))
|
||||
keyPos += 2
|
||||
}
|
||||
args = append(args, "streams")
|
||||
keyPos++
|
||||
for _, s := range a.Streams {
|
||||
args = append(args, s)
|
||||
}
|
||||
|
||||
cmd := NewXStreamSliceCmd(ctx, args...)
|
||||
if a.Block >= 0 {
|
||||
cmd.setReadTimeout(a.Block)
|
||||
}
|
||||
cmd.SetFirstKeyPos(keyPos)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XReadStreams(ctx context.Context, streams ...string) *XStreamSliceCmd {
|
||||
return c.XRead(ctx, &XReadArgs{
|
||||
Streams: streams,
|
||||
Block: -1,
|
||||
})
|
||||
}
|
||||
|
||||
func (c cmdable) XGroupCreate(ctx context.Context, stream, group, start string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XGroupCreateMkStream(ctx context.Context, stream, group, start string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "xgroup", "create", stream, group, start, "mkstream")
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XGroupSetID(ctx context.Context, stream, group, start string) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "xgroup", "setid", stream, group, start)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XGroupDestroy(ctx context.Context, stream, group string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "xgroup", "destroy", stream, group)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XGroupCreateConsumer(ctx context.Context, stream, group, consumer string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "xgroup", "createconsumer", stream, group, consumer)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XGroupDelConsumer(ctx context.Context, stream, group, consumer string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "xgroup", "delconsumer", stream, group, consumer)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type XReadGroupArgs struct {
|
||||
Group string
|
||||
Consumer string
|
||||
Streams []string // list of streams and ids, e.g. stream1 stream2 id1 id2
|
||||
Count int64
|
||||
Block time.Duration
|
||||
NoAck bool
|
||||
}
|
||||
|
||||
func (c cmdable) XReadGroup(ctx context.Context, a *XReadGroupArgs) *XStreamSliceCmd {
|
||||
args := make([]interface{}, 0, 10+len(a.Streams))
|
||||
args = append(args, "xreadgroup", "group", a.Group, a.Consumer)
|
||||
|
||||
keyPos := int8(4)
|
||||
if a.Count > 0 {
|
||||
args = append(args, "count", a.Count)
|
||||
keyPos += 2
|
||||
}
|
||||
if a.Block >= 0 {
|
||||
args = append(args, "block", int64(a.Block/time.Millisecond))
|
||||
keyPos += 2
|
||||
}
|
||||
if a.NoAck {
|
||||
args = append(args, "noack")
|
||||
keyPos++
|
||||
}
|
||||
args = append(args, "streams")
|
||||
keyPos++
|
||||
for _, s := range a.Streams {
|
||||
args = append(args, s)
|
||||
}
|
||||
|
||||
cmd := NewXStreamSliceCmd(ctx, args...)
|
||||
if a.Block >= 0 {
|
||||
cmd.setReadTimeout(a.Block)
|
||||
}
|
||||
cmd.SetFirstKeyPos(keyPos)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XAck(ctx context.Context, stream, group string, ids ...string) *IntCmd {
|
||||
args := []interface{}{"xack", stream, group}
|
||||
for _, id := range ids {
|
||||
args = append(args, id)
|
||||
}
|
||||
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)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type XPendingExtArgs struct {
|
||||
Stream string
|
||||
Group string
|
||||
Idle time.Duration
|
||||
Start string
|
||||
End string
|
||||
Count int64
|
||||
Consumer string
|
||||
}
|
||||
|
||||
func (c cmdable) XPendingExt(ctx context.Context, a *XPendingExtArgs) *XPendingExtCmd {
|
||||
args := make([]interface{}, 0, 9)
|
||||
args = append(args, "xpending", a.Stream, a.Group)
|
||||
if a.Idle != 0 {
|
||||
args = append(args, "idle", formatMs(ctx, a.Idle))
|
||||
}
|
||||
args = append(args, a.Start, a.End, a.Count)
|
||||
if a.Consumer != "" {
|
||||
args = append(args, a.Consumer)
|
||||
}
|
||||
cmd := NewXPendingExtCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type XAutoClaimArgs struct {
|
||||
Stream string
|
||||
Group string
|
||||
MinIdle time.Duration
|
||||
Start string
|
||||
Count int64
|
||||
Consumer string
|
||||
}
|
||||
|
||||
func (c cmdable) XAutoClaim(ctx context.Context, a *XAutoClaimArgs) *XAutoClaimCmd {
|
||||
args := xAutoClaimArgs(ctx, a)
|
||||
cmd := NewXAutoClaimCmd(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")
|
||||
cmd := NewXAutoClaimJustIDCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func xAutoClaimArgs(ctx context.Context, a *XAutoClaimArgs) []interface{} {
|
||||
args := make([]interface{}, 0, 8)
|
||||
args = append(args, "xautoclaim", a.Stream, a.Group, a.Consumer, formatMs(ctx, a.MinIdle), a.Start)
|
||||
if a.Count > 0 {
|
||||
args = append(args, "count", a.Count)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
type XClaimArgs struct {
|
||||
Stream string
|
||||
Group string
|
||||
Consumer string
|
||||
MinIdle time.Duration
|
||||
Messages []string
|
||||
}
|
||||
|
||||
func (c cmdable) XClaim(ctx context.Context, a *XClaimArgs) *XMessageSliceCmd {
|
||||
args := xClaimArgs(a)
|
||||
cmd := NewXMessageSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XClaimJustID(ctx context.Context, a *XClaimArgs) *StringSliceCmd {
|
||||
args := xClaimArgs(a)
|
||||
args = append(args, "justid")
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func xClaimArgs(a *XClaimArgs) []interface{} {
|
||||
args := make([]interface{}, 0, 5+len(a.Messages))
|
||||
args = append(args,
|
||||
"xclaim",
|
||||
a.Stream,
|
||||
a.Group, a.Consumer,
|
||||
int64(a.MinIdle/time.Millisecond))
|
||||
for _, id := range a.Messages {
|
||||
args = append(args, id)
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// xTrim If approx is true, add the "~" parameter, otherwise it is the default "=" (redis default).
|
||||
// example:
|
||||
//
|
||||
// XTRIM key MAXLEN/MINID threshold LIMIT limit.
|
||||
// XTRIM key MAXLEN/MINID ~ threshold LIMIT limit.
|
||||
//
|
||||
// The redis-server version is lower than 6.2, please set limit to 0.
|
||||
func (c cmdable) xTrim(
|
||||
ctx context.Context, key, strategy string,
|
||||
approx bool, threshold interface{}, limit int64,
|
||||
) *IntCmd {
|
||||
args := make([]interface{}, 0, 7)
|
||||
args = append(args, "xtrim", key, strategy)
|
||||
if approx {
|
||||
args = append(args, "~")
|
||||
}
|
||||
args = append(args, threshold)
|
||||
if limit > 0 {
|
||||
args = append(args, "limit", limit)
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// XTrimMaxLen No `~` rules are used, `limit` cannot be used.
|
||||
// cmd: XTRIM key MAXLEN maxLen
|
||||
func (c cmdable) XTrimMaxLen(ctx context.Context, key string, maxLen int64) *IntCmd {
|
||||
return c.xTrim(ctx, key, "maxlen", false, maxLen, 0)
|
||||
}
|
||||
|
||||
func (c cmdable) XTrimMaxLenApprox(ctx context.Context, key string, maxLen, limit int64) *IntCmd {
|
||||
return c.xTrim(ctx, key, "maxlen", true, maxLen, limit)
|
||||
}
|
||||
|
||||
func (c cmdable) XTrimMinID(ctx context.Context, key string, minID string) *IntCmd {
|
||||
return c.xTrim(ctx, key, "minid", false, minID, 0)
|
||||
}
|
||||
|
||||
func (c cmdable) XTrimMinIDApprox(ctx context.Context, key string, minID string, limit int64) *IntCmd {
|
||||
return c.xTrim(ctx, key, "minid", true, minID, limit)
|
||||
}
|
||||
|
||||
func (c cmdable) XInfoConsumers(ctx context.Context, key string, group string) *XInfoConsumersCmd {
|
||||
cmd := NewXInfoConsumersCmd(ctx, key, group)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XInfoGroups(ctx context.Context, key string) *XInfoGroupsCmd {
|
||||
cmd := NewXInfoGroupsCmd(ctx, key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) XInfoStream(ctx context.Context, key string) *XInfoStreamCmd {
|
||||
cmd := NewXInfoStreamCmd(ctx, key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// XInfoStreamFull XINFO STREAM FULL [COUNT count]
|
||||
// redis-server >= 6.0.
|
||||
func (c cmdable) XInfoStreamFull(ctx context.Context, key string, count int) *XInfoStreamFullCmd {
|
||||
args := make([]interface{}, 0, 6)
|
||||
args = append(args, "xinfo", "stream", key, "full")
|
||||
if count > 0 {
|
||||
args = append(args, "count", count)
|
||||
}
|
||||
cmd := NewXInfoStreamFullCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StringCmdable interface {
|
||||
Append(ctx context.Context, key, value string) *IntCmd
|
||||
Decr(ctx context.Context, key string) *IntCmd
|
||||
DecrBy(ctx context.Context, key string, decrement int64) *IntCmd
|
||||
Get(ctx context.Context, key string) *StringCmd
|
||||
GetRange(ctx context.Context, key string, start, end int64) *StringCmd
|
||||
GetSet(ctx context.Context, key string, value interface{}) *StringCmd
|
||||
GetEx(ctx context.Context, key string, expiration time.Duration) *StringCmd
|
||||
GetDel(ctx context.Context, key string) *StringCmd
|
||||
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
|
||||
LCS(ctx context.Context, q *LCSQuery) *LCSCmd
|
||||
MGet(ctx context.Context, keys ...string) *SliceCmd
|
||||
MSet(ctx context.Context, values ...interface{}) *StatusCmd
|
||||
MSetNX(ctx context.Context, values ...interface{}) *BoolCmd
|
||||
Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd
|
||||
SetArgs(ctx context.Context, key string, value interface{}, a SetArgs) *StatusCmd
|
||||
SetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd
|
||||
SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd
|
||||
SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd
|
||||
SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd
|
||||
StrLen(ctx context.Context, key string) *IntCmd
|
||||
}
|
||||
|
||||
func (c cmdable) Append(ctx context.Context, key, value string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "append", key, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Decr(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "decr", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) DecrBy(ctx context.Context, key string, decrement int64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "decrby", key, decrement)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Get Redis `GET key` command. It returns redis.Nil error when key does not exist.
|
||||
func (c cmdable) Get(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "get", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GetRange(ctx context.Context, key string, start, end int64) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "getrange", key, start, end)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) GetSet(ctx context.Context, key string, value interface{}) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "getset", key, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetEx An expiration of zero removes the TTL associated with the key (i.e. GETEX key persist).
|
||||
// Requires Redis >= 6.2.0.
|
||||
func (c cmdable) GetEx(ctx context.Context, key string, expiration time.Duration) *StringCmd {
|
||||
args := make([]interface{}, 0, 4)
|
||||
args = append(args, "getex", key)
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == 0 {
|
||||
args = append(args, "persist")
|
||||
}
|
||||
|
||||
cmd := NewStringCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// GetDel redis-server version >= 6.2.0.
|
||||
func (c cmdable) GetDel(ctx context.Context, key string) *StringCmd {
|
||||
cmd := NewStringCmd(ctx, "getdel", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) Incr(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "incr", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) IncrBy(ctx context.Context, key string, value int64) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "incrby", key, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) IncrByFloat(ctx context.Context, key string, value float64) *FloatCmd {
|
||||
cmd := NewFloatCmd(ctx, "incrbyfloat", key, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) LCS(ctx context.Context, q *LCSQuery) *LCSCmd {
|
||||
cmd := NewLCSCmd(ctx, q)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) MGet(ctx context.Context, keys ...string) *SliceCmd {
|
||||
args := make([]interface{}, 1+len(keys))
|
||||
args[0] = "mget"
|
||||
for i, key := range keys {
|
||||
args[1+i] = key
|
||||
}
|
||||
cmd := NewSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// MSet is like Set but accepts multiple values:
|
||||
// - MSet("key1", "value1", "key2", "value2")
|
||||
// - MSet([]string{"key1", "value1", "key2", "value2"})
|
||||
// - MSet(map[string]interface{}{"key1": "value1", "key2": "value2"})
|
||||
// - MSet(struct), For struct types, see HSet description.
|
||||
func (c cmdable) MSet(ctx context.Context, values ...interface{}) *StatusCmd {
|
||||
args := make([]interface{}, 1, 1+len(values))
|
||||
args[0] = "mset"
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// MSetNX is like SetNX but accepts multiple values:
|
||||
// - MSetNX("key1", "value1", "key2", "value2")
|
||||
// - MSetNX([]string{"key1", "value1", "key2", "value2"})
|
||||
// - MSetNX(map[string]interface{}{"key1": "value1", "key2": "value2"})
|
||||
// - MSetNX(struct), For struct types, see HSet description.
|
||||
func (c cmdable) MSetNX(ctx context.Context, values ...interface{}) *BoolCmd {
|
||||
args := make([]interface{}, 1, 1+len(values))
|
||||
args[0] = "msetnx"
|
||||
args = appendArgs(args, values)
|
||||
cmd := NewBoolCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// Set Redis `SET key value [expiration]` command.
|
||||
// Use expiration for `SETEx`-like behavior.
|
||||
//
|
||||
// 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.
|
||||
func (c cmdable) Set(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd {
|
||||
args := make([]interface{}, 3, 5)
|
||||
args[0] = "set"
|
||||
args[1] = key
|
||||
args[2] = value
|
||||
if expiration > 0 {
|
||||
if usePrecise(expiration) {
|
||||
args = append(args, "px", formatMs(ctx, expiration))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, expiration))
|
||||
}
|
||||
} else if expiration == KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetArgs provides arguments for the SetArgs function.
|
||||
type SetArgs struct {
|
||||
// Mode can be `NX` or `XX` or empty.
|
||||
Mode string
|
||||
|
||||
// Zero `TTL` or `Expiration` means that the key has no expiration time.
|
||||
TTL time.Duration
|
||||
ExpireAt time.Time
|
||||
|
||||
// When Get is true, the command returns the old value stored at key, or nil when key did not exist.
|
||||
Get bool
|
||||
|
||||
// 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.
|
||||
KeepTTL bool
|
||||
}
|
||||
|
||||
// SetArgs supports all the options that the SET command supports.
|
||||
// It is the alternative to the Set function when you want
|
||||
// to have more control over the options.
|
||||
func (c cmdable) SetArgs(ctx context.Context, key string, value interface{}, a SetArgs) *StatusCmd {
|
||||
args := []interface{}{"set", key, value}
|
||||
|
||||
if a.KeepTTL {
|
||||
args = append(args, "keepttl")
|
||||
}
|
||||
|
||||
if !a.ExpireAt.IsZero() {
|
||||
args = append(args, "exat", a.ExpireAt.Unix())
|
||||
}
|
||||
if a.TTL > 0 {
|
||||
if usePrecise(a.TTL) {
|
||||
args = append(args, "px", formatMs(ctx, a.TTL))
|
||||
} else {
|
||||
args = append(args, "ex", formatSec(ctx, a.TTL))
|
||||
}
|
||||
}
|
||||
|
||||
if a.Mode != "" {
|
||||
args = append(args, a.Mode)
|
||||
}
|
||||
|
||||
if a.Get {
|
||||
args = append(args, "get")
|
||||
}
|
||||
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetEx Redis `SETEx key expiration value` command.
|
||||
func (c cmdable) SetEx(ctx context.Context, key string, value interface{}, expiration time.Duration) *StatusCmd {
|
||||
cmd := NewStatusCmd(ctx, "setex", key, formatSec(ctx, expiration), value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetNX Redis `SET key value [expiration] NX` command.
|
||||
//
|
||||
// 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.
|
||||
func (c cmdable) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd {
|
||||
var cmd *BoolCmd
|
||||
switch expiration {
|
||||
case 0:
|
||||
// Use old `SETNX` to support old Redis versions.
|
||||
cmd = NewBoolCmd(ctx, "setnx", key, value)
|
||||
case KeepTTL:
|
||||
cmd = NewBoolCmd(ctx, "set", key, value, "keepttl", "nx")
|
||||
default:
|
||||
if usePrecise(expiration) {
|
||||
cmd = NewBoolCmd(ctx, "set", key, value, "px", formatMs(ctx, expiration), "nx")
|
||||
} else {
|
||||
cmd = NewBoolCmd(ctx, "set", key, value, "ex", formatSec(ctx, expiration), "nx")
|
||||
}
|
||||
}
|
||||
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// SetXX Redis `SET key value [expiration] XX` command.
|
||||
//
|
||||
// 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.
|
||||
func (c cmdable) SetXX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd {
|
||||
var cmd *BoolCmd
|
||||
switch expiration {
|
||||
case 0:
|
||||
cmd = NewBoolCmd(ctx, "set", key, value, "xx")
|
||||
case KeepTTL:
|
||||
cmd = NewBoolCmd(ctx, "set", key, value, "keepttl", "xx")
|
||||
default:
|
||||
if usePrecise(expiration) {
|
||||
cmd = NewBoolCmd(ctx, "set", key, value, "px", formatMs(ctx, expiration), "xx")
|
||||
} else {
|
||||
cmd = NewBoolCmd(ctx, "set", key, value, "ex", formatSec(ctx, expiration), "xx")
|
||||
}
|
||||
}
|
||||
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) SetRange(ctx context.Context, key string, offset int64, value string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "setrange", key, offset, value)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (c cmdable) StrLen(ctx context.Context, key string) *IntCmd {
|
||||
cmd := NewIntCmd(ctx, "strlen", key)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
+922
@@ -0,0 +1,922 @@
|
||||
package redis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
type TimeseriesCmdable interface {
|
||||
TSAdd(ctx context.Context, key string, timestamp interface{}, value float64) *IntCmd
|
||||
TSAddWithArgs(ctx context.Context, key string, timestamp interface{}, value float64, options *TSOptions) *IntCmd
|
||||
TSCreate(ctx context.Context, key string) *StatusCmd
|
||||
TSCreateWithArgs(ctx context.Context, key string, options *TSOptions) *StatusCmd
|
||||
TSAlter(ctx context.Context, key string, options *TSAlterOptions) *StatusCmd
|
||||
TSCreateRule(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int) *StatusCmd
|
||||
TSCreateRuleWithArgs(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int, options *TSCreateRuleOptions) *StatusCmd
|
||||
TSIncrBy(ctx context.Context, Key string, timestamp float64) *IntCmd
|
||||
TSIncrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd
|
||||
TSDecrBy(ctx context.Context, Key string, timestamp float64) *IntCmd
|
||||
TSDecrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd
|
||||
TSDel(ctx context.Context, Key string, fromTimestamp int, toTimestamp int) *IntCmd
|
||||
TSDeleteRule(ctx context.Context, sourceKey string, destKey string) *StatusCmd
|
||||
TSGet(ctx context.Context, key string) *TSTimestampValueCmd
|
||||
TSGetWithArgs(ctx context.Context, key string, options *TSGetOptions) *TSTimestampValueCmd
|
||||
TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd
|
||||
TSInfoWithArgs(ctx context.Context, key string, options *TSInfoOptions) *MapStringInterfaceCmd
|
||||
TSMAdd(ctx context.Context, ktvSlices [][]interface{}) *IntSliceCmd
|
||||
TSQueryIndex(ctx context.Context, filterExpr []string) *StringSliceCmd
|
||||
TSRevRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd
|
||||
TSRevRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRevRangeOptions) *TSTimestampValueSliceCmd
|
||||
TSRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd
|
||||
TSRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRangeOptions) *TSTimestampValueSliceCmd
|
||||
TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd
|
||||
TSMRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRangeOptions) *MapStringSliceInterfaceCmd
|
||||
TSMRevRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd
|
||||
TSMRevRangeWithArgs(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string, options *TSMRevRangeOptions) *MapStringSliceInterfaceCmd
|
||||
TSMGet(ctx context.Context, filters []string) *MapStringSliceInterfaceCmd
|
||||
TSMGetWithArgs(ctx context.Context, filters []string, options *TSMGetOptions) *MapStringSliceInterfaceCmd
|
||||
}
|
||||
|
||||
type TSOptions struct {
|
||||
Retention int
|
||||
ChunkSize int
|
||||
Encoding string
|
||||
DuplicatePolicy string
|
||||
Labels map[string]string
|
||||
}
|
||||
type TSIncrDecrOptions struct {
|
||||
Timestamp int64
|
||||
Retention int
|
||||
ChunkSize int
|
||||
Uncompressed bool
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
type TSAlterOptions struct {
|
||||
Retention int
|
||||
ChunkSize int
|
||||
DuplicatePolicy string
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
type TSCreateRuleOptions struct {
|
||||
alignTimestamp int64
|
||||
}
|
||||
|
||||
type TSGetOptions struct {
|
||||
Latest bool
|
||||
}
|
||||
|
||||
type TSInfoOptions struct {
|
||||
Debug bool
|
||||
}
|
||||
type Aggregator int
|
||||
|
||||
const (
|
||||
Invalid = Aggregator(iota)
|
||||
Avg
|
||||
Sum
|
||||
Min
|
||||
Max
|
||||
Range
|
||||
Count
|
||||
First
|
||||
Last
|
||||
StdP
|
||||
StdS
|
||||
VarP
|
||||
VarS
|
||||
Twa
|
||||
)
|
||||
|
||||
func (a Aggregator) String() string {
|
||||
switch a {
|
||||
case Invalid:
|
||||
return ""
|
||||
case Avg:
|
||||
return "AVG"
|
||||
case Sum:
|
||||
return "SUM"
|
||||
case Min:
|
||||
return "MIN"
|
||||
case Max:
|
||||
return "MAX"
|
||||
case Range:
|
||||
return "RANGE"
|
||||
case Count:
|
||||
return "COUNT"
|
||||
case First:
|
||||
return "FIRST"
|
||||
case Last:
|
||||
return "LAST"
|
||||
case StdP:
|
||||
return "STD.P"
|
||||
case StdS:
|
||||
return "STD.S"
|
||||
case VarP:
|
||||
return "VAR.P"
|
||||
case VarS:
|
||||
return "VAR.S"
|
||||
case Twa:
|
||||
return "TWA"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
type TSRangeOptions struct {
|
||||
Latest bool
|
||||
FilterByTS []int
|
||||
FilterByValue []int
|
||||
Count int
|
||||
Align interface{}
|
||||
Aggregator Aggregator
|
||||
BucketDuration int
|
||||
BucketTimestamp interface{}
|
||||
Empty bool
|
||||
}
|
||||
|
||||
type TSRevRangeOptions struct {
|
||||
Latest bool
|
||||
FilterByTS []int
|
||||
FilterByValue []int
|
||||
Count int
|
||||
Align interface{}
|
||||
Aggregator Aggregator
|
||||
BucketDuration int
|
||||
BucketTimestamp interface{}
|
||||
Empty bool
|
||||
}
|
||||
|
||||
type TSMRangeOptions struct {
|
||||
Latest bool
|
||||
FilterByTS []int
|
||||
FilterByValue []int
|
||||
WithLabels bool
|
||||
SelectedLabels []interface{}
|
||||
Count int
|
||||
Align interface{}
|
||||
Aggregator Aggregator
|
||||
BucketDuration int
|
||||
BucketTimestamp interface{}
|
||||
Empty bool
|
||||
GroupByLabel interface{}
|
||||
Reducer interface{}
|
||||
}
|
||||
|
||||
type TSMRevRangeOptions struct {
|
||||
Latest bool
|
||||
FilterByTS []int
|
||||
FilterByValue []int
|
||||
WithLabels bool
|
||||
SelectedLabels []interface{}
|
||||
Count int
|
||||
Align interface{}
|
||||
Aggregator Aggregator
|
||||
BucketDuration int
|
||||
BucketTimestamp interface{}
|
||||
Empty bool
|
||||
GroupByLabel interface{}
|
||||
Reducer interface{}
|
||||
}
|
||||
|
||||
type TSMGetOptions struct {
|
||||
Latest bool
|
||||
WithLabels bool
|
||||
SelectedLabels []interface{}
|
||||
}
|
||||
|
||||
// TSAdd - Adds one or more observations to a t-digest sketch.
|
||||
// For more information - https://redis.io/commands/ts.add/
|
||||
func (c cmdable) TSAdd(ctx context.Context, key string, timestamp interface{}, value float64) *IntCmd {
|
||||
args := []interface{}{"TS.ADD", key, timestamp, value}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSAddWithArgs - Adds one or more observations to a t-digest sketch.
|
||||
// This function also allows for specifying additional options such as:
|
||||
// Retention, ChunkSize, Encoding, DuplicatePolicy and Labels.
|
||||
// For more information - https://redis.io/commands/ts.add/
|
||||
func (c cmdable) TSAddWithArgs(ctx context.Context, key string, timestamp interface{}, value float64, options *TSOptions) *IntCmd {
|
||||
args := []interface{}{"TS.ADD", key, timestamp, value}
|
||||
if options != nil {
|
||||
if options.Retention != 0 {
|
||||
args = append(args, "RETENTION", options.Retention)
|
||||
}
|
||||
if options.ChunkSize != 0 {
|
||||
args = append(args, "CHUNK_SIZE", options.ChunkSize)
|
||||
}
|
||||
if options.Encoding != "" {
|
||||
args = append(args, "ENCODING", options.Encoding)
|
||||
}
|
||||
|
||||
if options.DuplicatePolicy != "" {
|
||||
args = append(args, "DUPLICATE_POLICY", options.DuplicatePolicy)
|
||||
}
|
||||
if options.Labels != nil {
|
||||
args = append(args, "LABELS")
|
||||
for label, value := range options.Labels {
|
||||
args = append(args, label, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSCreate - Creates a new time-series key.
|
||||
// For more information - https://redis.io/commands/ts.create/
|
||||
func (c cmdable) TSCreate(ctx context.Context, key string) *StatusCmd {
|
||||
args := []interface{}{"TS.CREATE", key}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSCreateWithArgs - Creates a new time-series key with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Retention, ChunkSize, Encoding, DuplicatePolicy and Labels.
|
||||
// For more information - https://redis.io/commands/ts.create/
|
||||
func (c cmdable) TSCreateWithArgs(ctx context.Context, key string, options *TSOptions) *StatusCmd {
|
||||
args := []interface{}{"TS.CREATE", key}
|
||||
if options != nil {
|
||||
if options.Retention != 0 {
|
||||
args = append(args, "RETENTION", options.Retention)
|
||||
}
|
||||
if options.ChunkSize != 0 {
|
||||
args = append(args, "CHUNK_SIZE", options.ChunkSize)
|
||||
}
|
||||
if options.Encoding != "" {
|
||||
args = append(args, "ENCODING", options.Encoding)
|
||||
}
|
||||
|
||||
if options.DuplicatePolicy != "" {
|
||||
args = append(args, "DUPLICATE_POLICY", options.DuplicatePolicy)
|
||||
}
|
||||
if options.Labels != nil {
|
||||
args = append(args, "LABELS")
|
||||
for label, value := range options.Labels {
|
||||
args = append(args, label, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSAlter - Alters an existing time-series key with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Retention, ChunkSize and DuplicatePolicy.
|
||||
// For more information - https://redis.io/commands/ts.alter/
|
||||
func (c cmdable) TSAlter(ctx context.Context, key string, options *TSAlterOptions) *StatusCmd {
|
||||
args := []interface{}{"TS.ALTER", key}
|
||||
if options != nil {
|
||||
if options.Retention != 0 {
|
||||
args = append(args, "RETENTION", options.Retention)
|
||||
}
|
||||
if options.ChunkSize != 0 {
|
||||
args = append(args, "CHUNK_SIZE", options.ChunkSize)
|
||||
}
|
||||
if options.DuplicatePolicy != "" {
|
||||
args = append(args, "DUPLICATE_POLICY", options.DuplicatePolicy)
|
||||
}
|
||||
if options.Labels != nil {
|
||||
args = append(args, "LABELS")
|
||||
for label, value := range options.Labels {
|
||||
args = append(args, label, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSCreateRule - Creates a compaction rule from sourceKey to destKey.
|
||||
// For more information - https://redis.io/commands/ts.createrule/
|
||||
func (c cmdable) TSCreateRule(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int) *StatusCmd {
|
||||
args := []interface{}{"TS.CREATERULE", sourceKey, destKey, "AGGREGATION", aggregator.String(), bucketDuration}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSCreateRuleWithArgs - Creates a compaction rule from sourceKey to destKey with additional option.
|
||||
// This function allows for specifying additional option such as:
|
||||
// alignTimestamp.
|
||||
// For more information - https://redis.io/commands/ts.createrule/
|
||||
func (c cmdable) TSCreateRuleWithArgs(ctx context.Context, sourceKey string, destKey string, aggregator Aggregator, bucketDuration int, options *TSCreateRuleOptions) *StatusCmd {
|
||||
args := []interface{}{"TS.CREATERULE", sourceKey, destKey, "AGGREGATION", aggregator.String(), bucketDuration}
|
||||
if options != nil {
|
||||
if options.alignTimestamp != 0 {
|
||||
args = append(args, options.alignTimestamp)
|
||||
}
|
||||
}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSIncrBy - Increments the value of a time-series key by the specified timestamp.
|
||||
// For more information - https://redis.io/commands/ts.incrby/
|
||||
func (c cmdable) TSIncrBy(ctx context.Context, Key string, timestamp float64) *IntCmd {
|
||||
args := []interface{}{"TS.INCRBY", Key, timestamp}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSIncrByWithArgs - Increments the value of a time-series key by the specified timestamp with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Timestamp, Retention, ChunkSize, Uncompressed and Labels.
|
||||
// For more information - https://redis.io/commands/ts.incrby/
|
||||
func (c cmdable) TSIncrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd {
|
||||
args := []interface{}{"TS.INCRBY", key, timestamp}
|
||||
if options != nil {
|
||||
if options.Timestamp != 0 {
|
||||
args = append(args, "TIMESTAMP", options.Timestamp)
|
||||
}
|
||||
if options.Retention != 0 {
|
||||
args = append(args, "RETENTION", options.Retention)
|
||||
}
|
||||
if options.ChunkSize != 0 {
|
||||
args = append(args, "CHUNK_SIZE", options.ChunkSize)
|
||||
}
|
||||
if options.Uncompressed {
|
||||
args = append(args, "UNCOMPRESSED")
|
||||
}
|
||||
if options.Labels != nil {
|
||||
args = append(args, "LABELS")
|
||||
for label, value := range options.Labels {
|
||||
args = append(args, label, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSDecrBy - Decrements the value of a time-series key by the specified timestamp.
|
||||
// For more information - https://redis.io/commands/ts.decrby/
|
||||
func (c cmdable) TSDecrBy(ctx context.Context, Key string, timestamp float64) *IntCmd {
|
||||
args := []interface{}{"TS.DECRBY", Key, timestamp}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSDecrByWithArgs - Decrements the value of a time-series key by the specified timestamp with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Timestamp, Retention, ChunkSize, Uncompressed and Labels.
|
||||
// For more information - https://redis.io/commands/ts.decrby/
|
||||
func (c cmdable) TSDecrByWithArgs(ctx context.Context, key string, timestamp float64, options *TSIncrDecrOptions) *IntCmd {
|
||||
args := []interface{}{"TS.DECRBY", key, timestamp}
|
||||
if options != nil {
|
||||
if options.Timestamp != 0 {
|
||||
args = append(args, "TIMESTAMP", options.Timestamp)
|
||||
}
|
||||
if options.Retention != 0 {
|
||||
args = append(args, "RETENTION", options.Retention)
|
||||
}
|
||||
if options.ChunkSize != 0 {
|
||||
args = append(args, "CHUNK_SIZE", options.ChunkSize)
|
||||
}
|
||||
if options.Uncompressed {
|
||||
args = append(args, "UNCOMPRESSED")
|
||||
}
|
||||
if options.Labels != nil {
|
||||
args = append(args, "LABELS")
|
||||
for label, value := range options.Labels {
|
||||
args = append(args, label, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSDel - Deletes a range of samples from a time-series key.
|
||||
// For more information - https://redis.io/commands/ts.del/
|
||||
func (c cmdable) TSDel(ctx context.Context, Key string, fromTimestamp int, toTimestamp int) *IntCmd {
|
||||
args := []interface{}{"TS.DEL", Key, fromTimestamp, toTimestamp}
|
||||
cmd := NewIntCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSDeleteRule - Deletes a compaction rule from sourceKey to destKey.
|
||||
// For more information - https://redis.io/commands/ts.deleterule/
|
||||
func (c cmdable) TSDeleteRule(ctx context.Context, sourceKey string, destKey string) *StatusCmd {
|
||||
args := []interface{}{"TS.DELETERULE", sourceKey, destKey}
|
||||
cmd := NewStatusCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSGetWithArgs - Gets the last sample of a time-series key with additional option.
|
||||
// This function allows for specifying additional option such as:
|
||||
// Latest.
|
||||
// For more information - https://redis.io/commands/ts.get/
|
||||
func (c cmdable) TSGetWithArgs(ctx context.Context, key string, options *TSGetOptions) *TSTimestampValueCmd {
|
||||
args := []interface{}{"TS.GET", key}
|
||||
if options != nil {
|
||||
if options.Latest {
|
||||
args = append(args, "LATEST")
|
||||
}
|
||||
}
|
||||
cmd := newTSTimestampValueCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSGet - Gets the last sample of a time-series key.
|
||||
// For more information - https://redis.io/commands/ts.get/
|
||||
func (c cmdable) TSGet(ctx context.Context, key string) *TSTimestampValueCmd {
|
||||
args := []interface{}{"TS.GET", key}
|
||||
cmd := newTSTimestampValueCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type TSTimestampValue struct {
|
||||
Timestamp int64
|
||||
Value float64
|
||||
}
|
||||
type TSTimestampValueCmd struct {
|
||||
baseCmd
|
||||
val TSTimestampValue
|
||||
}
|
||||
|
||||
func newTSTimestampValueCmd(ctx context.Context, args ...interface{}) *TSTimestampValueCmd {
|
||||
return &TSTimestampValueCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueCmd) SetVal(val TSTimestampValue) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueCmd) Result() (TSTimestampValue, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueCmd) Val() TSTimestampValue {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueCmd) readReply(rd *proto.Reader) (err error) {
|
||||
n, err := rd.ReadMapLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = TSTimestampValue{}
|
||||
for i := 0; i < n; i++ {
|
||||
timestamp, err := rd.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := rd.ReadString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val.Timestamp = timestamp
|
||||
cmd.val.Value, err = strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TSInfo - Returns information about a time-series key.
|
||||
// For more information - https://redis.io/commands/ts.info/
|
||||
func (c cmdable) TSInfo(ctx context.Context, key string) *MapStringInterfaceCmd {
|
||||
args := []interface{}{"TS.INFO", key}
|
||||
cmd := NewMapStringInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSInfoWithArgs - Returns information about a time-series key with additional option.
|
||||
// This function allows for specifying additional option such as:
|
||||
// Debug.
|
||||
// For more information - https://redis.io/commands/ts.info/
|
||||
func (c cmdable) TSInfoWithArgs(ctx context.Context, key string, options *TSInfoOptions) *MapStringInterfaceCmd {
|
||||
args := []interface{}{"TS.INFO", key}
|
||||
if options != nil {
|
||||
if options.Debug {
|
||||
args = append(args, "DEBUG")
|
||||
}
|
||||
}
|
||||
cmd := NewMapStringInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSMAdd - Adds multiple samples to multiple time-series keys.
|
||||
// It accepts a slice of 'ktv' slices, each containing exactly three elements: key, timestamp, and value.
|
||||
// This struct must be provided for this command to work.
|
||||
// For more information - https://redis.io/commands/ts.madd/
|
||||
func (c cmdable) TSMAdd(ctx context.Context, ktvSlices [][]interface{}) *IntSliceCmd {
|
||||
args := []interface{}{"TS.MADD"}
|
||||
for _, ktv := range ktvSlices {
|
||||
args = append(args, ktv...)
|
||||
}
|
||||
cmd := NewIntSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSQueryIndex - Returns all the keys matching the filter expression.
|
||||
// For more information - https://redis.io/commands/ts.queryindex/
|
||||
func (c cmdable) TSQueryIndex(ctx context.Context, filterExpr []string) *StringSliceCmd {
|
||||
args := []interface{}{"TS.QUERYINDEX"}
|
||||
for _, f := range filterExpr {
|
||||
args = append(args, f)
|
||||
}
|
||||
cmd := NewStringSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSRevRange - Returns a range of samples from a time-series key in reverse order.
|
||||
// For more information - https://redis.io/commands/ts.revrange/
|
||||
func (c cmdable) TSRevRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd {
|
||||
args := []interface{}{"TS.REVRANGE", key, fromTimestamp, toTimestamp}
|
||||
cmd := newTSTimestampValueSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSRevRangeWithArgs - Returns a range of samples from a time-series key in reverse order with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Latest, FilterByTS, FilterByValue, Count, Align, Aggregator,
|
||||
// BucketDuration, BucketTimestamp and Empty.
|
||||
// For more information - https://redis.io/commands/ts.revrange/
|
||||
func (c cmdable) TSRevRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRevRangeOptions) *TSTimestampValueSliceCmd {
|
||||
args := []interface{}{"TS.REVRANGE", key, fromTimestamp, toTimestamp}
|
||||
if options != nil {
|
||||
if options.Latest {
|
||||
args = append(args, "LATEST")
|
||||
}
|
||||
if options.FilterByTS != nil {
|
||||
args = append(args, "FILTER_BY_TS")
|
||||
for _, f := range options.FilterByTS {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.FilterByValue != nil {
|
||||
args = append(args, "FILTER_BY_VALUE")
|
||||
for _, f := range options.FilterByValue {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.Count != 0 {
|
||||
args = append(args, "COUNT", options.Count)
|
||||
}
|
||||
if options.Align != nil {
|
||||
args = append(args, "ALIGN", options.Align)
|
||||
}
|
||||
if options.Aggregator != 0 {
|
||||
args = append(args, "AGGREGATION", options.Aggregator.String())
|
||||
}
|
||||
if options.BucketDuration != 0 {
|
||||
args = append(args, options.BucketDuration)
|
||||
}
|
||||
if options.BucketTimestamp != nil {
|
||||
args = append(args, "BUCKETTIMESTAMP", options.BucketTimestamp)
|
||||
}
|
||||
if options.Empty {
|
||||
args = append(args, "EMPTY")
|
||||
}
|
||||
}
|
||||
cmd := newTSTimestampValueSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSRange - Returns a range of samples from a time-series key.
|
||||
// For more information - https://redis.io/commands/ts.range/
|
||||
func (c cmdable) TSRange(ctx context.Context, key string, fromTimestamp int, toTimestamp int) *TSTimestampValueSliceCmd {
|
||||
args := []interface{}{"TS.RANGE", key, fromTimestamp, toTimestamp}
|
||||
cmd := newTSTimestampValueSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSRangeWithArgs - Returns a range of samples from a time-series key with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Latest, FilterByTS, FilterByValue, Count, Align, Aggregator,
|
||||
// BucketDuration, BucketTimestamp and Empty.
|
||||
// For more information - https://redis.io/commands/ts.range/
|
||||
func (c cmdable) TSRangeWithArgs(ctx context.Context, key string, fromTimestamp int, toTimestamp int, options *TSRangeOptions) *TSTimestampValueSliceCmd {
|
||||
args := []interface{}{"TS.RANGE", key, fromTimestamp, toTimestamp}
|
||||
if options != nil {
|
||||
if options.Latest {
|
||||
args = append(args, "LATEST")
|
||||
}
|
||||
if options.FilterByTS != nil {
|
||||
args = append(args, "FILTER_BY_TS")
|
||||
for _, f := range options.FilterByTS {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.FilterByValue != nil {
|
||||
args = append(args, "FILTER_BY_VALUE")
|
||||
for _, f := range options.FilterByValue {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.Count != 0 {
|
||||
args = append(args, "COUNT", options.Count)
|
||||
}
|
||||
if options.Align != nil {
|
||||
args = append(args, "ALIGN", options.Align)
|
||||
}
|
||||
if options.Aggregator != 0 {
|
||||
args = append(args, "AGGREGATION", options.Aggregator.String())
|
||||
}
|
||||
if options.BucketDuration != 0 {
|
||||
args = append(args, options.BucketDuration)
|
||||
}
|
||||
if options.BucketTimestamp != nil {
|
||||
args = append(args, "BUCKETTIMESTAMP", options.BucketTimestamp)
|
||||
}
|
||||
if options.Empty {
|
||||
args = append(args, "EMPTY")
|
||||
}
|
||||
}
|
||||
cmd := newTSTimestampValueSliceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
type TSTimestampValueSliceCmd struct {
|
||||
baseCmd
|
||||
val []TSTimestampValue
|
||||
}
|
||||
|
||||
func newTSTimestampValueSliceCmd(ctx context.Context, args ...interface{}) *TSTimestampValueSliceCmd {
|
||||
return &TSTimestampValueSliceCmd{
|
||||
baseCmd: baseCmd{
|
||||
ctx: ctx,
|
||||
args: args,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueSliceCmd) String() string {
|
||||
return cmdString(cmd, cmd.val)
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueSliceCmd) SetVal(val []TSTimestampValue) {
|
||||
cmd.val = val
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueSliceCmd) Result() ([]TSTimestampValue, error) {
|
||||
return cmd.val, cmd.err
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueSliceCmd) Val() []TSTimestampValue {
|
||||
return cmd.val
|
||||
}
|
||||
|
||||
func (cmd *TSTimestampValueSliceCmd) readReply(rd *proto.Reader) (err error) {
|
||||
n, err := rd.ReadArrayLen()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val = make([]TSTimestampValue, n)
|
||||
for i := 0; i < n; i++ {
|
||||
_, _ = rd.ReadArrayLen()
|
||||
timestamp, err := rd.ReadInt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value, err := rd.ReadString()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.val[i].Timestamp = timestamp
|
||||
cmd.val[i].Value, err = strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TSMRange - Returns a range of samples from multiple time-series keys.
|
||||
// For more information - https://redis.io/commands/ts.mrange/
|
||||
func (c cmdable) TSMRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd {
|
||||
args := []interface{}{"TS.MRANGE", fromTimestamp, toTimestamp, "FILTER"}
|
||||
for _, f := range filterExpr {
|
||||
args = append(args, f)
|
||||
}
|
||||
cmd := NewMapStringSliceInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSMRangeWithArgs - Returns a range of samples from multiple time-series keys with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Latest, FilterByTS, FilterByValue, WithLabels, SelectedLabels,
|
||||
// Count, Align, Aggregator, BucketDuration, BucketTimestamp,
|
||||
// Empty, GroupByLabel and Reducer.
|
||||
// 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}
|
||||
if options != nil {
|
||||
if options.Latest {
|
||||
args = append(args, "LATEST")
|
||||
}
|
||||
if options.FilterByTS != nil {
|
||||
args = append(args, "FILTER_BY_TS")
|
||||
for _, f := range options.FilterByTS {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.FilterByValue != nil {
|
||||
args = append(args, "FILTER_BY_VALUE")
|
||||
for _, f := range options.FilterByValue {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.WithLabels {
|
||||
args = append(args, "WITHLABELS")
|
||||
}
|
||||
if options.SelectedLabels != nil {
|
||||
args = append(args, "SELECTED_LABELS")
|
||||
args = append(args, options.SelectedLabels...)
|
||||
}
|
||||
if options.Count != 0 {
|
||||
args = append(args, "COUNT", options.Count)
|
||||
}
|
||||
if options.Align != nil {
|
||||
args = append(args, "ALIGN", options.Align)
|
||||
}
|
||||
if options.Aggregator != 0 {
|
||||
args = append(args, "AGGREGATION", options.Aggregator.String())
|
||||
}
|
||||
if options.BucketDuration != 0 {
|
||||
args = append(args, options.BucketDuration)
|
||||
}
|
||||
if options.BucketTimestamp != nil {
|
||||
args = append(args, "BUCKETTIMESTAMP", options.BucketTimestamp)
|
||||
}
|
||||
if options.Empty {
|
||||
args = append(args, "EMPTY")
|
||||
}
|
||||
}
|
||||
args = append(args, "FILTER")
|
||||
for _, f := range filterExpr {
|
||||
args = append(args, f)
|
||||
}
|
||||
if options != nil {
|
||||
if options.GroupByLabel != nil {
|
||||
args = append(args, "GROUPBY", options.GroupByLabel)
|
||||
}
|
||||
if options.Reducer != nil {
|
||||
args = append(args, "REDUCE", options.Reducer)
|
||||
}
|
||||
}
|
||||
cmd := NewMapStringSliceInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSMRevRange - Returns a range of samples from multiple time-series keys in reverse order.
|
||||
// For more information - https://redis.io/commands/ts.mrevrange/
|
||||
func (c cmdable) TSMRevRange(ctx context.Context, fromTimestamp int, toTimestamp int, filterExpr []string) *MapStringSliceInterfaceCmd {
|
||||
args := []interface{}{"TS.MREVRANGE", fromTimestamp, toTimestamp, "FILTER"}
|
||||
for _, f := range filterExpr {
|
||||
args = append(args, f)
|
||||
}
|
||||
cmd := NewMapStringSliceInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSMRevRangeWithArgs - Returns a range of samples from multiple time-series keys in reverse order with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Latest, FilterByTS, FilterByValue, WithLabels, SelectedLabels,
|
||||
// Count, Align, Aggregator, BucketDuration, BucketTimestamp,
|
||||
// Empty, GroupByLabel and Reducer.
|
||||
// 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}
|
||||
if options != nil {
|
||||
if options.Latest {
|
||||
args = append(args, "LATEST")
|
||||
}
|
||||
if options.FilterByTS != nil {
|
||||
args = append(args, "FILTER_BY_TS")
|
||||
for _, f := range options.FilterByTS {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.FilterByValue != nil {
|
||||
args = append(args, "FILTER_BY_VALUE")
|
||||
for _, f := range options.FilterByValue {
|
||||
args = append(args, f)
|
||||
}
|
||||
}
|
||||
if options.WithLabels {
|
||||
args = append(args, "WITHLABELS")
|
||||
}
|
||||
if options.SelectedLabels != nil {
|
||||
args = append(args, "SELECTED_LABELS")
|
||||
args = append(args, options.SelectedLabels...)
|
||||
}
|
||||
if options.Count != 0 {
|
||||
args = append(args, "COUNT", options.Count)
|
||||
}
|
||||
if options.Align != nil {
|
||||
args = append(args, "ALIGN", options.Align)
|
||||
}
|
||||
if options.Aggregator != 0 {
|
||||
args = append(args, "AGGREGATION", options.Aggregator.String())
|
||||
}
|
||||
if options.BucketDuration != 0 {
|
||||
args = append(args, options.BucketDuration)
|
||||
}
|
||||
if options.BucketTimestamp != nil {
|
||||
args = append(args, "BUCKETTIMESTAMP", options.BucketTimestamp)
|
||||
}
|
||||
if options.Empty {
|
||||
args = append(args, "EMPTY")
|
||||
}
|
||||
}
|
||||
args = append(args, "FILTER")
|
||||
for _, f := range filterExpr {
|
||||
args = append(args, f)
|
||||
}
|
||||
if options != nil {
|
||||
if options.GroupByLabel != nil {
|
||||
args = append(args, "GROUPBY", options.GroupByLabel)
|
||||
}
|
||||
if options.Reducer != nil {
|
||||
args = append(args, "REDUCE", options.Reducer)
|
||||
}
|
||||
}
|
||||
cmd := NewMapStringSliceInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSMGet - Returns the last sample of multiple time-series keys.
|
||||
// For more information - https://redis.io/commands/ts.mget/
|
||||
func (c cmdable) TSMGet(ctx context.Context, filters []string) *MapStringSliceInterfaceCmd {
|
||||
args := []interface{}{"TS.MGET", "FILTER"}
|
||||
for _, f := range filters {
|
||||
args = append(args, f)
|
||||
}
|
||||
cmd := NewMapStringSliceInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
// TSMGetWithArgs - Returns the last sample of multiple time-series keys with additional options.
|
||||
// This function allows for specifying additional options such as:
|
||||
// Latest, WithLabels and SelectedLabels.
|
||||
// For more information - https://redis.io/commands/ts.mget/
|
||||
func (c cmdable) TSMGetWithArgs(ctx context.Context, filters []string, options *TSMGetOptions) *MapStringSliceInterfaceCmd {
|
||||
args := []interface{}{"TS.MGET"}
|
||||
if options != nil {
|
||||
if options.Latest {
|
||||
args = append(args, "LATEST")
|
||||
}
|
||||
if options.WithLabels {
|
||||
args = append(args, "WITHLABELS")
|
||||
}
|
||||
if options.SelectedLabels != nil {
|
||||
args = append(args, "SELECTED_LABELS")
|
||||
args = append(args, options.SelectedLabels...)
|
||||
}
|
||||
}
|
||||
args = append(args, "FILTER")
|
||||
for _, f := range filters {
|
||||
args = append(args, f)
|
||||
}
|
||||
cmd := NewMapStringSliceInterfaceCmd(ctx, args...)
|
||||
_ = c(ctx, cmd)
|
||||
return cmd
|
||||
}
|
||||
Generated
Vendored
+29
-27
@@ -3,8 +3,8 @@ package redis
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-redis/redis/v8/internal/pool"
|
||||
"github.com/go-redis/redis/v8/internal/proto"
|
||||
"github.com/redis/go-redis/v9/internal/pool"
|
||||
"github.com/redis/go-redis/v9/internal/proto"
|
||||
)
|
||||
|
||||
// TxFailedErr transaction redis failed.
|
||||
@@ -19,18 +19,16 @@ type Tx struct {
|
||||
baseClient
|
||||
cmdable
|
||||
statefulCmdable
|
||||
hooks
|
||||
ctx context.Context
|
||||
hooksMixin
|
||||
}
|
||||
|
||||
func (c *Client) newTx(ctx context.Context) *Tx {
|
||||
func (c *Client) newTx() *Tx {
|
||||
tx := Tx{
|
||||
baseClient: baseClient{
|
||||
opt: c.opt,
|
||||
connPool: pool.NewStickyConnPool(c.connPool),
|
||||
},
|
||||
hooks: c.hooks.clone(),
|
||||
ctx: ctx,
|
||||
hooksMixin: c.hooksMixin.clone(),
|
||||
}
|
||||
tx.init()
|
||||
return &tx
|
||||
@@ -39,25 +37,19 @@ func (c *Client) newTx(ctx context.Context) *Tx {
|
||||
func (c *Tx) init() {
|
||||
c.cmdable = c.Process
|
||||
c.statefulCmdable = c.Process
|
||||
}
|
||||
|
||||
func (c *Tx) Context() context.Context {
|
||||
return c.ctx
|
||||
}
|
||||
|
||||
func (c *Tx) WithContext(ctx context.Context) *Tx {
|
||||
if ctx == nil {
|
||||
panic("nil context")
|
||||
}
|
||||
clone := *c
|
||||
clone.init()
|
||||
clone.hooks.lock()
|
||||
clone.ctx = ctx
|
||||
return &clone
|
||||
c.initHooks(hooks{
|
||||
dial: c.baseClient.dial,
|
||||
process: c.baseClient.process,
|
||||
pipeline: c.baseClient.processPipeline,
|
||||
txPipeline: c.baseClient.processTxPipeline,
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Tx) Process(ctx context.Context, cmd Cmder) error {
|
||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
||||
err := c.processHook(ctx, cmd)
|
||||
cmd.SetErr(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch prepares a transaction and marks the keys to be watched
|
||||
@@ -65,7 +57,7 @@ func (c *Tx) Process(ctx context.Context, cmd Cmder) error {
|
||||
//
|
||||
// The transaction is automatically closed when fn exits.
|
||||
func (c *Client) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
|
||||
tx := c.newTx(ctx)
|
||||
tx := c.newTx()
|
||||
defer tx.Close(ctx)
|
||||
if len(keys) > 0 {
|
||||
if err := tx.Watch(ctx, keys...).Err(); err != nil {
|
||||
@@ -109,9 +101,8 @@ func (c *Tx) Unwatch(ctx context.Context, keys ...string) *StatusCmd {
|
||||
// Pipeline creates a pipeline. Usually it is more convenient to use Pipelined.
|
||||
func (c *Tx) Pipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processPipeline(ctx, cmds, c.baseClient.processPipeline)
|
||||
return c.processPipelineHook(ctx, cmds)
|
||||
},
|
||||
}
|
||||
pipe.init()
|
||||
@@ -139,11 +130,22 @@ func (c *Tx) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder
|
||||
// TxPipeline creates a pipeline. Usually it is more convenient to use TxPipelined.
|
||||
func (c *Tx) TxPipeline() Pipeliner {
|
||||
pipe := Pipeline{
|
||||
ctx: c.ctx,
|
||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||
return c.hooks.processTxPipeline(ctx, cmds, c.baseClient.processTxPipeline)
|
||||
cmds = wrapMultiExec(ctx, cmds)
|
||||
return c.processTxPipelineHook(ctx, cmds)
|
||||
},
|
||||
}
|
||||
pipe.init()
|
||||
return &pipe
|
||||
}
|
||||
|
||||
func wrapMultiExec(ctx context.Context, cmds []Cmder) []Cmder {
|
||||
if len(cmds) == 0 {
|
||||
panic("not reached")
|
||||
}
|
||||
cmdsCopy := make([]Cmder, len(cmds)+2)
|
||||
cmdsCopy[0] = NewStatusCmd(ctx, "multi")
|
||||
copy(cmdsCopy[1:], cmds)
|
||||
cmdsCopy[len(cmdsCopy)-1] = NewSliceCmd(ctx, "exec")
|
||||
return cmdsCopy
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user