dependencies: Update go-redis from v8 to v9 (#7041)
Updated so we can access the `SetAddrs()` method added to `*redis.Ring` in `v9` in #7042. Part of #5545
This commit is contained in:
parent
e7f78291ba
commit
4ed54ff9c6
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/jmhodges/clock"
|
"github.com/jmhodges/clock"
|
||||||
capb "github.com/letsencrypt/boulder/ca/proto"
|
capb "github.com/letsencrypt/boulder/ca/proto"
|
||||||
"github.com/letsencrypt/boulder/cmd"
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
|
@ -18,6 +17,7 @@ import (
|
||||||
"github.com/letsencrypt/boulder/sa"
|
"github.com/letsencrypt/boulder/sa"
|
||||||
"github.com/letsencrypt/boulder/test"
|
"github.com/letsencrypt/boulder/test"
|
||||||
"github.com/letsencrypt/boulder/test/vars"
|
"github.com/letsencrypt/boulder/test/vars"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,11 +21,11 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-logr/stdr"
|
"github.com/go-logr/stdr"
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/go-sql-driver/mysql"
|
"github.com/go-sql-driver/mysql"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/collectors"
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"go.opentelemetry.io/otel"
|
"go.opentelemetry.io/otel"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
|
||||||
"go.opentelemetry.io/otel/propagation"
|
"go.opentelemetry.io/otel/propagation"
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -9,7 +9,6 @@ require (
|
||||||
github.com/aws/smithy-go v1.14.1
|
github.com/aws/smithy-go v1.14.1
|
||||||
github.com/eggsampler/acme/v3 v3.4.0
|
github.com/eggsampler/acme/v3 v3.4.0
|
||||||
github.com/go-logr/stdr v1.2.2
|
github.com/go-logr/stdr v1.2.2
|
||||||
github.com/go-redis/redis/v8 v8.11.5
|
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||||
github.com/google/certificate-transparency-go v1.1.6
|
github.com/google/certificate-transparency-go v1.1.6
|
||||||
|
|
@ -24,6 +23,7 @@ require (
|
||||||
github.com/miekg/pkcs11 v1.1.1
|
github.com/miekg/pkcs11 v1.1.1
|
||||||
github.com/prometheus/client_golang v1.15.1
|
github.com/prometheus/client_golang v1.15.1
|
||||||
github.com/prometheus/client_model v0.4.0
|
github.com/prometheus/client_model v0.4.0
|
||||||
|
github.com/redis/go-redis/v9 v9.1.0
|
||||||
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399
|
github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399
|
||||||
github.com/weppos/publicsuffix-go v0.30.1-0.20230620154423-38c92ad2d5c6
|
github.com/weppos/publicsuffix-go v0.30.1-0.20230620154423-38c92ad2d5c6
|
||||||
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300
|
github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300
|
||||||
|
|
@ -66,6 +66,7 @@ require (
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||||
github.com/go-logr/logr v1.2.4 // indirect
|
github.com/go-logr/logr v1.2.4 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
|
|
||||||
12
go.sum
12
go.sum
|
|
@ -90,6 +90,8 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
|
||||||
|
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
|
||||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
|
||||||
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||||
|
|
@ -137,6 +139,7 @@ github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBd
|
||||||
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
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 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
|
@ -155,8 +158,6 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
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-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
|
|
@ -292,10 +293,7 @@ github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1
|
||||||
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
|
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w=
|
github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w=
|
||||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
|
||||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
|
||||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||||
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
|
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
|
||||||
|
|
@ -324,6 +322,8 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
|
||||||
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
|
||||||
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
|
||||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||||
|
github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
|
||||||
|
github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
|
||||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
|
|
@ -534,6 +534,7 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
|
@ -755,7 +756,6 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.3.0/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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import (
|
||||||
|
|
||||||
bredis "github.com/letsencrypt/boulder/redis"
|
bredis "github.com/letsencrypt/boulder/redis"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/jmhodges/clock"
|
"github.com/jmhodges/clock"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Compile-time check that RedisSource implements the source interface.
|
// Compile-time check that RedisSource implements the source interface.
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"github.com/letsencrypt/boulder/test"
|
"github.com/letsencrypt/boulder/test"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/jmhodges/clock"
|
"github.com/jmhodges/clock"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
func newTestRedisSource(clk clock.FakeClock, addrs map[string]string) *RedisSource {
|
func newTestRedisSource(clk clock.FakeClock, addrs map[string]string) *RedisSource {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package redis
|
package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
// An interface satisfied by *redis.ClusterClient and also by a mock in our tests.
|
// An interface satisfied by *redis.ClusterClient and also by a mock in our tests.
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/letsencrypt/boulder/metrics"
|
"github.com/letsencrypt/boulder/metrics"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockPoolStatGetter struct{}
|
type mockPoolStatGetter struct{}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/jmhodges/clock"
|
"github.com/jmhodges/clock"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
|
|
||||||
"github.com/letsencrypt/boulder/cmd"
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
|
|
@ -92,6 +92,7 @@ type RedisConfig struct {
|
||||||
// Default is 1 minute. -1 disables idle connections reaper,
|
// Default is 1 minute. -1 disables idle connections reaper,
|
||||||
// but idle connections are still discarded by the client
|
// but idle connections are still discarded by the client
|
||||||
// if IdleTimeout is set.
|
// if IdleTimeout is set.
|
||||||
|
// Deprecated: This field has been deprecated and will be removed.
|
||||||
IdleCheckFrequency config.Duration `validate:"-"`
|
IdleCheckFrequency config.Duration `validate:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,12 +121,11 @@ func MakeClient(c *RedisConfig, clk clock.Clock, stats prometheus.Registerer) (*
|
||||||
ReadTimeout: c.ReadTimeout.Duration,
|
ReadTimeout: c.ReadTimeout.Duration,
|
||||||
WriteTimeout: c.WriteTimeout.Duration,
|
WriteTimeout: c.WriteTimeout.Duration,
|
||||||
|
|
||||||
PoolSize: c.PoolSize,
|
PoolSize: c.PoolSize,
|
||||||
MinIdleConns: c.MinIdleConns,
|
MinIdleConns: c.MinIdleConns,
|
||||||
MaxConnAge: c.MaxConnAge.Duration,
|
ConnMaxLifetime: c.MaxConnAge.Duration,
|
||||||
PoolTimeout: c.PoolTimeout.Duration,
|
PoolTimeout: c.PoolTimeout.Duration,
|
||||||
IdleTimeout: c.IdleTimeout.Duration,
|
ConnMaxIdleTime: c.IdleTimeout.Duration,
|
||||||
IdleCheckFrequency: c.IdleCheckFrequency.Duration,
|
|
||||||
})
|
})
|
||||||
return rocsp.NewWritingClient(rdb, c.Timeout.Duration, clk, stats), nil
|
return rocsp.NewWritingClient(rdb, c.Timeout.Duration, clk, stats), nil
|
||||||
}
|
}
|
||||||
|
|
@ -160,12 +160,11 @@ func MakeReadClient(c *RedisConfig, clk clock.Clock, stats prometheus.Registerer
|
||||||
DialTimeout: c.DialTimeout.Duration,
|
DialTimeout: c.DialTimeout.Duration,
|
||||||
ReadTimeout: c.ReadTimeout.Duration,
|
ReadTimeout: c.ReadTimeout.Duration,
|
||||||
|
|
||||||
PoolSize: c.PoolSize,
|
PoolSize: c.PoolSize,
|
||||||
MinIdleConns: c.MinIdleConns,
|
MinIdleConns: c.MinIdleConns,
|
||||||
MaxConnAge: c.MaxConnAge.Duration,
|
ConnMaxLifetime: c.MaxConnAge.Duration,
|
||||||
PoolTimeout: c.PoolTimeout.Duration,
|
PoolTimeout: c.PoolTimeout.Duration,
|
||||||
IdleTimeout: c.IdleTimeout.Duration,
|
ConnMaxIdleTime: c.IdleTimeout.Duration,
|
||||||
IdleCheckFrequency: c.IdleCheckFrequency.Duration,
|
|
||||||
})
|
})
|
||||||
return rocsp.NewReadingClient(rdb, c.Timeout.Duration, clk, stats), nil
|
return rocsp.NewReadingClient(rdb, c.Timeout.Duration, clk, stats), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"github.com/letsencrypt/boulder/core"
|
"github.com/letsencrypt/boulder/core"
|
||||||
bredis "github.com/letsencrypt/boulder/redis"
|
bredis "github.com/letsencrypt/boulder/redis"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/jmhodges/clock"
|
"github.com/jmhodges/clock"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"github.com/jmhodges/clock"
|
"github.com/jmhodges/clock"
|
||||||
"github.com/letsencrypt/boulder/cmd"
|
"github.com/letsencrypt/boulder/cmd"
|
||||||
"github.com/letsencrypt/boulder/metrics"
|
"github.com/letsencrypt/boulder/metrics"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
"golang.org/x/crypto/ocsp"
|
"golang.org/x/crypto/ocsp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
*.rdb
|
|
||||||
testdata/*/
|
|
||||||
.idea/
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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>
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
@ -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)},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
*.rdb
|
||||||
|
testdata/*
|
||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
testdata/redis:
|
||||||
|
mkdir -p $@
|
||||||
|
wget -qO- https://download.redis.io/releases/redis-7.2-rc3.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/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
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
# 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](example/otel) example which demonstrates how you can use Uptrace to monitor
|
||||||
|
> go-redis.
|
||||||
|
|
||||||
|
## 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"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ctx = context.Background()
|
||||||
|
|
||||||
|
func ExampleClient() {
|
||||||
|
url := "redis://localhost:6379?password=hello&protocol=3"
|
||||||
|
opts, err := redis.ParseURL(url)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
rdb := redis.NewClient(opts)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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>
|
||||||
|
|
@ -6,17 +6,19 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
|
"net/url"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
"github.com/redis/go-redis/v9/internal"
|
||||||
"github.com/go-redis/redis/v8/internal/hashtag"
|
"github.com/redis/go-redis/v9/internal/hashtag"
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
"github.com/redis/go-redis/v9/internal/pool"
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
"github.com/redis/go-redis/v9/internal/proto"
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
"github.com/redis/go-redis/v9/internal/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errClusterNoNodes = fmt.Errorf("redis: cluster has no nodes")
|
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.
|
// A seed list of host:port addresses of cluster nodes.
|
||||||
Addrs []string
|
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 creates a cluster node client with provided name and options.
|
||||||
NewClient func(opt *Options) *Client
|
NewClient func(opt *Options) *Client
|
||||||
|
|
||||||
|
|
@ -57,6 +62,7 @@ type ClusterOptions struct {
|
||||||
|
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
OnConnect func(ctx context.Context, cn *Conn) error
|
||||||
|
|
||||||
|
Protocol int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
|
||||||
|
|
@ -64,20 +70,18 @@ type ClusterOptions struct {
|
||||||
MinRetryBackoff time.Duration
|
MinRetryBackoff time.Duration
|
||||||
MaxRetryBackoff time.Duration
|
MaxRetryBackoff time.Duration
|
||||||
|
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
WriteTimeout 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
|
||||||
// PoolSize applies per cluster node and not for the whole cluster.
|
MinIdleConns int
|
||||||
PoolSize int
|
MaxIdleConns int
|
||||||
MinIdleConns int
|
ConnMaxIdleTime time.Duration
|
||||||
MaxConnAge time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
PoolTimeout time.Duration
|
|
||||||
IdleTimeout time.Duration
|
|
||||||
IdleCheckFrequency time.Duration
|
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
@ -131,13 +135,137 @@ 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.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 {
|
func (opt *ClusterOptions) clientOptions() *Options {
|
||||||
const disableIdleCheck = -1
|
|
||||||
|
|
||||||
return &Options{
|
return &Options{
|
||||||
Dialer: opt.Dialer,
|
ClientName: opt.ClientName,
|
||||||
OnConnect: opt.OnConnect,
|
Dialer: opt.Dialer,
|
||||||
|
OnConnect: opt.OnConnect,
|
||||||
|
|
||||||
|
Protocol: opt.Protocol,
|
||||||
Username: opt.Username,
|
Username: opt.Username,
|
||||||
Password: opt.Password,
|
Password: opt.Password,
|
||||||
|
|
||||||
|
|
@ -149,13 +277,13 @@ func (opt *ClusterOptions) clientOptions() *Options {
|
||||||
ReadTimeout: opt.ReadTimeout,
|
ReadTimeout: opt.ReadTimeout,
|
||||||
WriteTimeout: opt.WriteTimeout,
|
WriteTimeout: opt.WriteTimeout,
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
PoolFIFO: opt.PoolFIFO,
|
||||||
PoolSize: opt.PoolSize,
|
PoolSize: opt.PoolSize,
|
||||||
MinIdleConns: opt.MinIdleConns,
|
PoolTimeout: opt.PoolTimeout,
|
||||||
MaxConnAge: opt.MaxConnAge,
|
MinIdleConns: opt.MinIdleConns,
|
||||||
PoolTimeout: opt.PoolTimeout,
|
MaxIdleConns: opt.MaxIdleConns,
|
||||||
IdleTimeout: opt.IdleTimeout,
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||||
IdleCheckFrequency: disableIdleCheck,
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
TLSConfig: opt.TLSConfig,
|
||||||
// If ClusterSlots is populated, then we probably have an artificial
|
// If ClusterSlots is populated, then we probably have an artificial
|
||||||
|
|
@ -204,15 +332,26 @@ func (n *clusterNode) updateLatency() {
|
||||||
const numProbe = 10
|
const numProbe = 10
|
||||||
var dur uint64
|
var dur uint64
|
||||||
|
|
||||||
|
successes := 0
|
||||||
for i := 0; i < numProbe; i++ {
|
for i := 0; i < numProbe; i++ {
|
||||||
time.Sleep(time.Duration(10+rand.Intn(10)) * time.Millisecond)
|
time.Sleep(time.Duration(10+rand.Intn(10)) * time.Millisecond)
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
n.Client.Ping(context.TODO())
|
err := n.Client.Ping(context.TODO()).Err()
|
||||||
dur += uint64(time.Since(start) / time.Microsecond)
|
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))
|
atomic.StoreUint32(&n.latency, uint32(latency+0.5))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -262,6 +401,7 @@ type clusterNodes struct {
|
||||||
nodes map[string]*clusterNode
|
nodes map[string]*clusterNode
|
||||||
activeAddrs []string
|
activeAddrs []string
|
||||||
closed bool
|
closed bool
|
||||||
|
onNewNode []func(rdb *Client)
|
||||||
|
|
||||||
_generation uint32 // atomic
|
_generation uint32 // atomic
|
||||||
}
|
}
|
||||||
|
|
@ -297,6 +437,12 @@ func (c *clusterNodes) Close() error {
|
||||||
return firstErr
|
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) {
|
func (c *clusterNodes) Addrs() ([]string, error) {
|
||||||
var addrs []string
|
var addrs []string
|
||||||
|
|
||||||
|
|
@ -374,6 +520,9 @@ func (c *clusterNodes) GetOrCreate(addr string) (*clusterNode, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
node = newClusterNode(c.opt, addr)
|
node = newClusterNode(c.opt, addr)
|
||||||
|
for _, fn := range c.onNewNode {
|
||||||
|
fn(node.Client)
|
||||||
|
}
|
||||||
|
|
||||||
c.addrs = appendIfNotExists(c.addrs, addr)
|
c.addrs = appendIfNotExists(c.addrs, addr)
|
||||||
c.nodes[addr] = node
|
c.nodes[addr] = node
|
||||||
|
|
@ -683,21 +832,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
|
// ClusterClient is a Redis Cluster client representing a pool of zero
|
||||||
// or more underlying connections. It's safe for concurrent use by
|
// or more underlying connections. It's safe for concurrent use by
|
||||||
// multiple goroutines.
|
// multiple goroutines.
|
||||||
type ClusterClient struct {
|
type ClusterClient struct {
|
||||||
*clusterClient
|
opt *ClusterOptions
|
||||||
|
nodes *clusterNodes
|
||||||
|
state *clusterStateHolder
|
||||||
|
cmdsInfoCache *cmdsInfoCache
|
||||||
cmdable
|
cmdable
|
||||||
hooks
|
hooksMixin
|
||||||
ctx context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClusterClient returns a Redis Cluster client as described in
|
// NewClusterClient returns a Redis Cluster client as described in
|
||||||
|
|
@ -706,38 +850,24 @@ func NewClusterClient(opt *ClusterOptions) *ClusterClient {
|
||||||
opt.init()
|
opt.init()
|
||||||
|
|
||||||
c := &ClusterClient{
|
c := &ClusterClient{
|
||||||
clusterClient: &clusterClient{
|
opt: opt,
|
||||||
opt: opt,
|
nodes: newClusterNodes(opt),
|
||||||
nodes: newClusterNodes(opt),
|
|
||||||
},
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.state = newClusterStateHolder(c.loadState)
|
c.state = newClusterStateHolder(c.loadState)
|
||||||
c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo)
|
c.cmdsInfoCache = newCmdsInfoCache(c.cmdsInfo)
|
||||||
c.cmdable = c.Process
|
c.cmdable = c.Process
|
||||||
|
|
||||||
if opt.IdleCheckFrequency > 0 {
|
c.initHooks(hooks{
|
||||||
go c.reaper(opt.IdleCheckFrequency)
|
dial: nil,
|
||||||
}
|
process: c.process,
|
||||||
|
pipeline: c.processPipeline,
|
||||||
|
txPipeline: c.processTxPipeline,
|
||||||
|
})
|
||||||
|
|
||||||
return c
|
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.
|
// Options returns read-only Options that were used to create the client.
|
||||||
func (c *ClusterClient) Options() *ClusterOptions {
|
func (c *ClusterClient) Options() *ClusterOptions {
|
||||||
return c.opt
|
return c.opt
|
||||||
|
|
@ -757,7 +887,7 @@ func (c *ClusterClient) Close() error {
|
||||||
return c.nodes.Close()
|
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 {
|
func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||||
cmd := NewCmd(ctx, args...)
|
cmd := NewCmd(ctx, args...)
|
||||||
_ = c.Process(ctx, cmd)
|
_ = c.Process(ctx, cmd)
|
||||||
|
|
@ -765,13 +895,14 @@ func (c *ClusterClient) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) Process(ctx context.Context, cmd Cmder) error {
|
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 {
|
func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||||
cmdInfo := c.cmdInfo(cmd.Name())
|
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||||
slot := c.cmdSlot(cmd)
|
slot := c.cmdSlot(ctx, cmd)
|
||||||
|
|
||||||
var node *clusterNode
|
var node *clusterNode
|
||||||
var ask bool
|
var ask bool
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
@ -791,12 +922,12 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ask {
|
if ask {
|
||||||
|
ask = false
|
||||||
|
|
||||||
pipe := node.Client.Pipeline()
|
pipe := node.Client.Pipeline()
|
||||||
_ = pipe.Process(ctx, NewCmd(ctx, "asking"))
|
_ = pipe.Process(ctx, NewCmd(ctx, "asking"))
|
||||||
_ = pipe.Process(ctx, cmd)
|
_ = pipe.Process(ctx, cmd)
|
||||||
_, lastErr = pipe.Exec(ctx)
|
_, lastErr = pipe.Exec(ctx)
|
||||||
_ = pipe.Close()
|
|
||||||
ask = false
|
|
||||||
} else {
|
} else {
|
||||||
lastErr = node.Client.Process(ctx, cmd)
|
lastErr = node.Client.Process(ctx, cmd)
|
||||||
}
|
}
|
||||||
|
|
@ -851,6 +982,10 @@ func (c *ClusterClient) process(ctx context.Context, cmd Cmder) error {
|
||||||
return lastErr
|
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.
|
// ForEachMaster concurrently calls the fn on each master node in the cluster.
|
||||||
// It returns the first error if any.
|
// It returns the first error if any.
|
||||||
func (c *ClusterClient) ForEachMaster(
|
func (c *ClusterClient) ForEachMaster(
|
||||||
|
|
@ -1056,30 +1191,9 @@ func (c *ClusterClient) loadState(ctx context.Context) (*clusterState, error) {
|
||||||
return nil, firstErr
|
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 {
|
func (c *ClusterClient) Pipeline() Pipeliner {
|
||||||
pipe := Pipeline{
|
pipe := Pipeline{
|
||||||
ctx: c.ctx,
|
exec: pipelineExecer(c.processPipelineHook),
|
||||||
exec: c.processPipeline,
|
|
||||||
}
|
}
|
||||||
pipe.init()
|
pipe.init()
|
||||||
return &pipe
|
return &pipe
|
||||||
|
|
@ -1090,13 +1204,9 @@ func (c *ClusterClient) Pipelined(ctx context.Context, fn func(Pipeliner) error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) processPipeline(ctx context.Context, cmds []Cmder) 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()
|
cmdsMap := newCmdsMap()
|
||||||
err := c.mapCmdsByNode(ctx, cmdsMap, cmds)
|
|
||||||
if err != nil {
|
if err := c.mapCmdsByNode(ctx, cmdsMap, cmds); err != nil {
|
||||||
setCmdsErr(cmds, err)
|
setCmdsErr(cmds, err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -1116,18 +1226,7 @@ func (c *ClusterClient) _processPipeline(ctx context.Context, cmds []Cmder) erro
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(node *clusterNode, cmds []Cmder) {
|
go func(node *clusterNode, cmds []Cmder) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
c.processPipelineNode(ctx, node, cmds, failedCmds)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}(node, cmds)
|
}(node, cmds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1147,9 +1246,9 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.opt.ReadOnly && c.cmdsAreReadOnly(cmds) {
|
if c.opt.ReadOnly && c.cmdsAreReadOnly(ctx, cmds) {
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
slot := c.cmdSlot(cmd)
|
slot := c.cmdSlot(ctx, cmd)
|
||||||
node, err := c.slotReadOnlyNode(state, slot)
|
node, err := c.slotReadOnlyNode(state, slot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -1160,7 +1259,7 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
slot := c.cmdSlot(cmd)
|
slot := c.cmdSlot(ctx, cmd)
|
||||||
node, err := state.slotMasterNode(slot)
|
node, err := state.slotMasterNode(slot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -1170,9 +1269,9 @@ func (c *ClusterClient) mapCmdsByNode(ctx context.Context, cmdsMap *cmdsMap, cmd
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) cmdsAreReadOnly(cmds []Cmder) bool {
|
func (c *ClusterClient) cmdsAreReadOnly(ctx context.Context, cmds []Cmder) bool {
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
cmdInfo := c.cmdInfo(cmd.Name())
|
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||||
if cmdInfo == nil || !cmdInfo.ReadOnly {
|
if cmdInfo == nil || !cmdInfo.ReadOnly {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
@ -1180,22 +1279,42 @@ func (c *ClusterClient) cmdsAreReadOnly(cmds []Cmder) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) _processPipelineNode(
|
func (c *ClusterClient) processPipelineNode(
|
||||||
ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap,
|
ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap,
|
||||||
) error {
|
) {
|
||||||
return node.Client.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
_ = node.Client.withProcessPipelineHook(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
||||||
return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
cn, err := node.Client.getConn(ctx)
|
||||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
if err != nil {
|
||||||
return writeCmds(wr, cmds)
|
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||||
})
|
setCmdsErr(cmds, err)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
var processErr error
|
||||||
return c.pipelineReadCmds(ctx, node, rd, cmds, failedCmds)
|
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 +1325,7 @@ func (c *ClusterClient) pipelineReadCmds(
|
||||||
cmds []Cmder,
|
cmds []Cmder,
|
||||||
failedCmds *cmdsMap,
|
failedCmds *cmdsMap,
|
||||||
) error {
|
) error {
|
||||||
for _, cmd := range cmds {
|
for i, cmd := range cmds {
|
||||||
err := cmd.readReply(rd)
|
err := cmd.readReply(rd)
|
||||||
cmd.SetErr(err)
|
cmd.SetErr(err)
|
||||||
|
|
||||||
|
|
@ -1218,15 +1337,24 @@ func (c *ClusterClient) pipelineReadCmds(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.opt.ReadOnly && isLoadingError(err) {
|
if c.opt.ReadOnly {
|
||||||
node.MarkAsFailing()
|
node.MarkAsFailing()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isRedisError(err) {
|
||||||
|
if shouldRetry(err, true) {
|
||||||
|
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||||
|
}
|
||||||
|
setCmdsErr(cmds[i+1:], err)
|
||||||
return 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 err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1260,8 +1388,10 @@ func (c *ClusterClient) checkMovedErr(
|
||||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
||||||
func (c *ClusterClient) TxPipeline() Pipeliner {
|
func (c *ClusterClient) TxPipeline() Pipeliner {
|
||||||
pipe := Pipeline{
|
pipe := Pipeline{
|
||||||
ctx: c.ctx,
|
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||||
exec: c.processTxPipeline,
|
cmds = wrapMultiExec(ctx, cmds)
|
||||||
|
return c.processTxPipelineHook(ctx, cmds)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
pipe.init()
|
pipe.init()
|
||||||
return &pipe
|
return &pipe
|
||||||
|
|
@ -1272,10 +1402,6 @@ func (c *ClusterClient) TxPipelined(ctx context.Context, fn func(Pipeliner) erro
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
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.
|
// Trim multi .. exec.
|
||||||
cmds = cmds[1 : len(cmds)-1]
|
cmds = cmds[1 : len(cmds)-1]
|
||||||
|
|
||||||
|
|
@ -1285,7 +1411,7 @@ func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) er
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdsMap := c.mapCmdsBySlot(cmds)
|
cmdsMap := c.mapCmdsBySlot(ctx, cmds)
|
||||||
for slot, cmds := range cmdsMap {
|
for slot, cmds := range cmdsMap {
|
||||||
node, err := state.slotMasterNode(slot)
|
node, err := state.slotMasterNode(slot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -1309,19 +1435,7 @@ func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) er
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func(node *clusterNode, cmds []Cmder) {
|
go func(node *clusterNode, cmds []Cmder) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
c.processTxPipelineNode(ctx, node, cmds, failedCmds)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}(node, cmds)
|
}(node, cmds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1336,44 +1450,69 @@ func (c *ClusterClient) _processTxPipeline(ctx context.Context, cmds []Cmder) er
|
||||||
return cmdsFirstErr(cmds)
|
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)
|
cmdsMap := make(map[int][]Cmder)
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
slot := c.cmdSlot(cmd)
|
slot := c.cmdSlot(ctx, cmd)
|
||||||
cmdsMap[slot] = append(cmdsMap[slot], cmd)
|
cmdsMap[slot] = append(cmdsMap[slot], cmd)
|
||||||
}
|
}
|
||||||
return cmdsMap
|
return cmdsMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) _processTxPipelineNode(
|
func (c *ClusterClient) processTxPipelineNode(
|
||||||
ctx context.Context, node *clusterNode, cmds []Cmder, failedCmds *cmdsMap,
|
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 {
|
) error {
|
||||||
return node.Client.hooks.processTxPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
if err := cn.WithWriter(c.context(ctx), c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
||||||
return node.Client.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
return writeCmds(wr, cmds)
|
||||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
}); err != nil {
|
||||||
return writeCmds(wr, cmds)
|
if shouldRetry(err, true) {
|
||||||
})
|
_ = c.mapCmdsByNode(ctx, failedCmds, cmds)
|
||||||
if err != nil {
|
}
|
||||||
return err
|
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 {
|
return err
|
||||||
statusCmd := cmds[0].(*StatusCmd)
|
}
|
||||||
// Trim multi and exec.
|
|
||||||
cmds = cmds[1 : len(cmds)-1]
|
|
||||||
|
|
||||||
err := c.txPipelineReadQueued(ctx, rd, statusCmd, cmds, failedCmds)
|
return pipelineReadCmds(rd, trimmedCmds)
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1406,12 +1545,7 @@ func (c *ClusterClient) txPipelineReadQueued(
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch line[0] {
|
if line[0] != proto.RespArray {
|
||||||
case proto.ErrorReply:
|
|
||||||
return proto.ParseErrorReply(line)
|
|
||||||
case proto.ArrayReply:
|
|
||||||
// ok
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("redis: expected '*', but got line %q", line)
|
return fmt.Errorf("redis: expected '*', but got line %q", line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1568,6 +1702,15 @@ func (c *ClusterClient) PSubscribe(ctx context.Context, channels ...string) *Pub
|
||||||
return pubsub
|
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 {
|
func (c *ClusterClient) retryBackoff(attempt int) time.Duration {
|
||||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
|
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
|
||||||
}
|
}
|
||||||
|
|
@ -1614,26 +1757,27 @@ func (c *ClusterClient) cmdsInfo(ctx context.Context) (map[string]*CommandInfo,
|
||||||
return nil, firstErr
|
return nil, firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) cmdInfo(name string) *CommandInfo {
|
func (c *ClusterClient) cmdInfo(ctx context.Context, name string) *CommandInfo {
|
||||||
cmdsInfo, err := c.cmdsInfoCache.Get(c.ctx)
|
cmdsInfo, err := c.cmdsInfoCache.Get(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
internal.Logger.Printf(context.TODO(), "getting command info: %s", err)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
info := cmdsInfo[name]
|
info := cmdsInfo[name]
|
||||||
if info == nil {
|
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
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClusterClient) cmdSlot(cmd Cmder) int {
|
func (c *ClusterClient) cmdSlot(ctx context.Context, cmd Cmder) int {
|
||||||
args := cmd.Args()
|
args := cmd.Args()
|
||||||
if args[0] == "cluster" && args[1] == "getkeysinslot" {
|
if args[0] == "cluster" && args[1] == "getkeysinslot" {
|
||||||
return args[2].(int)
|
return args[2].(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdInfo := c.cmdInfo(cmd.Name())
|
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||||
return cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo))
|
return cmdSlot(cmd, cmdFirstKeyPos(cmd, cmdInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1661,7 +1805,7 @@ func (c *ClusterClient) cmdNode(
|
||||||
return state.slotMasterNode(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 {
|
if c.opt.RouteByLatency {
|
||||||
return state.slotClosestNode(slot)
|
return state.slotClosestNode(slot)
|
||||||
}
|
}
|
||||||
|
|
@ -1708,6 +1852,13 @@ func (c *ClusterClient) MasterForKey(ctx context.Context, key string) (*Client,
|
||||||
return node.Client, err
|
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 {
|
func appendUniqueNode(nodes []*clusterNode, node *clusterNode) []*clusterNode {
|
||||||
for _, n := range nodes {
|
for _, n := range nodes {
|
||||||
if n == node {
|
if n == node {
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd {
|
func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd {
|
||||||
cmd := NewIntCmd(ctx, "dbsize")
|
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
|
var size int64
|
||||||
err := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error {
|
err := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error {
|
||||||
n, err := master.DBSize(ctx).Result()
|
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 {
|
func (c *ClusterClient) ScriptLoad(ctx context.Context, script string) *StringCmd {
|
||||||
cmd := NewStringCmd(ctx, "script", "load", script)
|
cmd := NewStringCmd(ctx, "script", "load", script)
|
||||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||||
mu := &sync.Mutex{}
|
var mu sync.Mutex
|
||||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
||||||
val, err := shard.ScriptLoad(ctx, script).Result()
|
val, err := shard.ScriptLoad(ctx, script).Result()
|
||||||
if err != nil {
|
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 {
|
func (c *ClusterClient) ScriptFlush(ctx context.Context) *StatusCmd {
|
||||||
cmd := NewStatusCmd(ctx, "script", "flush")
|
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 {
|
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
||||||
return shard.ScriptFlush(ctx).Err()
|
return shard.ScriptFlush(ctx).Err()
|
||||||
})
|
})
|
||||||
|
|
@ -82,8 +82,8 @@ func (c *ClusterClient) ScriptExists(ctx context.Context, hashes ...string) *Boo
|
||||||
result[i] = true
|
result[i] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
_ = c.withProcessHook(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
||||||
mu := &sync.Mutex{}
|
var mu sync.Mutex
|
||||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
||||||
val, err := shard.ScriptExists(ctx, hashes...).Result()
|
val, err := shard.ScriptExists(ctx, hashes...).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -6,13 +6,24 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
"github.com/redis/go-redis/v9/internal/pool"
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
"github.com/redis/go-redis/v9/internal/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrClosed performs any operation on the closed client will return this error.
|
// ErrClosed performs any operation on the closed client will return this error.
|
||||||
var ErrClosed = pool.ErrClosed
|
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 {
|
||||||
|
err, ok := err.(Error)
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
msg := err.Error()
|
||||||
|
msg = strings.TrimPrefix(msg, "ERR ") // KVRocks adds such prefix
|
||||||
|
return strings.HasPrefix(msg, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
type Error interface {
|
type Error interface {
|
||||||
error
|
error
|
||||||
|
|
||||||
|
|
@ -91,7 +102,7 @@ func isBadConn(err error, allowTimeout bool, addr string) bool {
|
||||||
|
|
||||||
if allowTimeout {
|
if allowTimeout {
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
||||||
return !netErr.Temporary()
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func AppendArg(b []byte, v interface{}) []byte {
|
func AppendArg(b []byte, v interface{}) []byte {
|
||||||
|
|
@ -11,7 +13,7 @@ func AppendArg(b []byte, v interface{}) []byte {
|
||||||
case nil:
|
case nil:
|
||||||
return append(b, "<nil>"...)
|
return append(b, "<nil>"...)
|
||||||
case string:
|
case string:
|
||||||
return appendUTF8String(b, Bytes(v))
|
return appendUTF8String(b, util.StringToBytes(v))
|
||||||
case []byte:
|
case []byte:
|
||||||
return appendUTF8String(b, v)
|
return appendUTF8String(b, v)
|
||||||
case int:
|
case int:
|
||||||
|
|
@ -3,7 +3,7 @@ package hashtag
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
"github.com/redis/go-redis/v9/internal/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
const slotNumber = 16384
|
const slotNumber = 16384
|
||||||
|
|
@ -10,6 +10,12 @@ import (
|
||||||
// decoderFunc represents decoding functions for default built-in types.
|
// decoderFunc represents decoding functions for default built-in types.
|
||||||
type decoderFunc func(reflect.Value, string) error
|
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 (
|
var (
|
||||||
// List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1).
|
// List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1).
|
||||||
decoders = []decoderFunc{
|
decoders = []decoderFunc{
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
package hscan
|
package hscan
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// structMap contains the map of struct fields for target structs
|
// structMap contains the map of struct fields for target structs
|
||||||
|
|
@ -84,7 +87,32 @@ func (s StructValue) Scan(key string, value string) error {
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
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()
|
t := s.value.Type()
|
||||||
return fmt.Errorf("cannot scan redis.result %s into struct field %s.%s of type %s, error-%s",
|
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())
|
value, t.Name(), t.Field(field.index).Name, t.Field(field.index).Type, err.Error())
|
||||||
|
|
@ -3,7 +3,7 @@ package internal
|
||||||
import (
|
import (
|
||||||
"time"
|
"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 {
|
func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration {
|
||||||
|
|
@ -32,7 +32,9 @@ type Once struct {
|
||||||
|
|
||||||
// Do calls the function f if and only if Do has not been invoked
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// 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
|
// is niladic, it may be necessary to use a function literal to capture the
|
||||||
// arguments to a function to be invoked by Do:
|
// 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 {
|
func (o *Once) Do(f func() error) error {
|
||||||
if atomic.LoadUint32(&o.done) == 1 {
|
if atomic.LoadUint32(&o.done) == 1 {
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
"github.com/redis/go-redis/v9/internal/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var noDeadline = time.Time{}
|
var noDeadline = time.Time{}
|
||||||
|
|
@ -63,9 +63,13 @@ func (cn *Conn) RemoteAddr() net.Addr {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cn *Conn) WithReader(ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error) error {
|
func (cn *Conn) WithReader(
|
||||||
if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil {
|
ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error,
|
||||||
return err
|
) error {
|
||||||
|
if timeout >= 0 {
|
||||||
|
if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return fn(cn.rd)
|
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(
|
func (cn *Conn) WithWriter(
|
||||||
ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error,
|
ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error,
|
||||||
) error {
|
) error {
|
||||||
if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil {
|
if timeout >= 0 {
|
||||||
return err
|
if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if cn.bw.Buffered() > 0 {
|
if cn.bw.Buffered() > 0 {
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
//go:build linux || darwin || dragonfly || freebsd || netbsd || openbsd || solaris || illumos
|
||||||
|
// +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
|
||||||
|
}
|
||||||
10
vendor/github.com/redis/go-redis/v9/internal/pool/conn_check_dummy.go
generated
vendored
Normal file
10
vendor/github.com/redis/go-redis/v9/internal/pool/conn_check_dummy.go
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
//go:build !linux && !darwin && !dragonfly && !freebsd && !netbsd && !openbsd && !solaris && !illumos
|
||||||
|
// +build !linux,!darwin,!dragonfly,!freebsd,!netbsd,!openbsd,!solaris,!illumos
|
||||||
|
|
||||||
|
package pool
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
func connCheck(conn net.Conn) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
"github.com/redis/go-redis/v9/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -54,16 +54,15 @@ type Pooler interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
Dialer func(context.Context) (net.Conn, error)
|
Dialer func(context.Context) (net.Conn, error)
|
||||||
OnClose func(*Conn) error
|
|
||||||
|
|
||||||
PoolFIFO bool
|
PoolFIFO bool
|
||||||
PoolSize int
|
PoolSize int
|
||||||
MinIdleConns int
|
PoolTimeout time.Duration
|
||||||
MaxConnAge time.Duration
|
MinIdleConns int
|
||||||
PoolTimeout time.Duration
|
MaxIdleConns int
|
||||||
IdleTimeout time.Duration
|
ConnMaxIdleTime time.Duration
|
||||||
IdleCheckFrequency time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type lastDialErrorWrap struct {
|
type lastDialErrorWrap struct {
|
||||||
|
|
@ -71,66 +70,67 @@ type lastDialErrorWrap struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnPool struct {
|
type ConnPool struct {
|
||||||
opt *Options
|
cfg *Options
|
||||||
|
|
||||||
dialErrorsNum uint32 // atomic
|
dialErrorsNum uint32 // atomic
|
||||||
|
|
||||||
lastDialError atomic.Value
|
lastDialError atomic.Value
|
||||||
|
|
||||||
queue chan struct{}
|
queue chan struct{}
|
||||||
|
|
||||||
connsMu sync.Mutex
|
connsMu sync.Mutex
|
||||||
conns []*Conn
|
conns []*Conn
|
||||||
idleConns []*Conn
|
idleConns []*Conn
|
||||||
|
|
||||||
poolSize int
|
poolSize int
|
||||||
idleConnsLen int
|
idleConnsLen int
|
||||||
|
|
||||||
stats Stats
|
stats Stats
|
||||||
|
|
||||||
_closed uint32 // atomic
|
_closed uint32 // atomic
|
||||||
closedCh chan struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Pooler = (*ConnPool)(nil)
|
var _ Pooler = (*ConnPool)(nil)
|
||||||
|
|
||||||
func NewConnPool(opt *Options) *ConnPool {
|
func NewConnPool(opt *Options) *ConnPool {
|
||||||
p := &ConnPool{
|
p := &ConnPool{
|
||||||
opt: opt,
|
cfg: opt,
|
||||||
|
|
||||||
queue: make(chan struct{}, opt.PoolSize),
|
queue: make(chan struct{}, opt.PoolSize),
|
||||||
conns: make([]*Conn, 0, opt.PoolSize),
|
conns: make([]*Conn, 0, opt.PoolSize),
|
||||||
idleConns: make([]*Conn, 0, opt.PoolSize),
|
idleConns: make([]*Conn, 0, opt.PoolSize),
|
||||||
closedCh: make(chan struct{}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.connsMu.Lock()
|
p.connsMu.Lock()
|
||||||
p.checkMinIdleConns()
|
p.checkMinIdleConns()
|
||||||
p.connsMu.Unlock()
|
p.connsMu.Unlock()
|
||||||
|
|
||||||
if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
|
|
||||||
go p.reaper(opt.IdleCheckFrequency)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ConnPool) checkMinIdleConns() {
|
func (p *ConnPool) checkMinIdleConns() {
|
||||||
if p.opt.MinIdleConns == 0 {
|
if p.cfg.MinIdleConns == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for p.poolSize < p.opt.PoolSize && p.idleConnsLen < p.opt.MinIdleConns {
|
for p.poolSize < p.cfg.PoolSize && p.idleConnsLen < p.cfg.MinIdleConns {
|
||||||
p.poolSize++
|
select {
|
||||||
p.idleConnsLen++
|
case p.queue <- struct{}{}:
|
||||||
|
p.poolSize++
|
||||||
|
p.idleConnsLen++
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := p.addIdleConn()
|
err := p.addIdleConn()
|
||||||
if err != nil && err != ErrClosed {
|
if err != nil && err != ErrClosed {
|
||||||
p.connsMu.Lock()
|
p.connsMu.Lock()
|
||||||
p.poolSize--
|
p.poolSize--
|
||||||
p.idleConnsLen--
|
p.idleConnsLen--
|
||||||
p.connsMu.Unlock()
|
p.connsMu.Unlock()
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
p.freeTurn()
|
||||||
|
}()
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,7 +176,7 @@ func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
|
||||||
p.conns = append(p.conns, cn)
|
p.conns = append(p.conns, cn)
|
||||||
if pooled {
|
if pooled {
|
||||||
// If pool is full remove the cn on next Put.
|
// 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
|
cn.pooled = false
|
||||||
} else {
|
} else {
|
||||||
p.poolSize++
|
p.poolSize++
|
||||||
|
|
@ -191,14 +191,14 @@ func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
|
||||||
return nil, ErrClosed
|
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()
|
return nil, p.getLastDialError()
|
||||||
}
|
}
|
||||||
|
|
||||||
netConn, err := p.opt.Dialer(ctx)
|
netConn, err := p.cfg.Dialer(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.setLastDialError(err)
|
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()
|
go p.tryDial()
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -215,7 +215,7 @@ func (p *ConnPool) tryDial() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := p.opt.Dialer(context.Background())
|
conn, err := p.cfg.Dialer(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.setLastDialError(err)
|
p.setLastDialError(err)
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
|
|
@ -263,7 +263,7 @@ func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
if p.isStaleConn(cn) {
|
if !p.isHealthyConn(cn) {
|
||||||
_ = p.CloseConn(cn)
|
_ = p.CloseConn(cn)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -283,10 +283,6 @@ func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
|
||||||
return newcn, nil
|
return newcn, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ConnPool) getTurn() {
|
|
||||||
p.queue <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) waitTurn(ctx context.Context) error {
|
func (p *ConnPool) waitTurn(ctx context.Context) error {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|
@ -301,7 +297,7 @@ func (p *ConnPool) waitTurn(ctx context.Context) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
timer := timers.Get().(*time.Timer)
|
timer := timers.Get().(*time.Timer)
|
||||||
timer.Reset(p.opt.PoolTimeout)
|
timer.Reset(p.cfg.PoolTimeout)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
|
@ -337,7 +333,7 @@ func (p *ConnPool) popIdle() (*Conn, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var cn *Conn
|
var cn *Conn
|
||||||
if p.opt.PoolFIFO {
|
if p.cfg.PoolFIFO {
|
||||||
cn = p.idleConns[0]
|
cn = p.idleConns[0]
|
||||||
copy(p.idleConns, p.idleConns[1:])
|
copy(p.idleConns, p.idleConns[1:])
|
||||||
p.idleConns = p.idleConns[:n-1]
|
p.idleConns = p.idleConns[:n-1]
|
||||||
|
|
@ -363,14 +359,28 @@ func (p *ConnPool) Put(ctx context.Context, cn *Conn) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldCloseConn bool
|
||||||
|
|
||||||
p.connsMu.Lock()
|
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.connsMu.Unlock()
|
||||||
|
|
||||||
p.freeTurn()
|
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.removeConnWithLock(cn)
|
||||||
p.freeTurn()
|
p.freeTurn()
|
||||||
_ = p.closeConn(cn)
|
_ = p.closeConn(cn)
|
||||||
|
|
@ -383,8 +393,8 @@ func (p *ConnPool) CloseConn(cn *Conn) error {
|
||||||
|
|
||||||
func (p *ConnPool) removeConnWithLock(cn *Conn) {
|
func (p *ConnPool) removeConnWithLock(cn *Conn) {
|
||||||
p.connsMu.Lock()
|
p.connsMu.Lock()
|
||||||
|
defer p.connsMu.Unlock()
|
||||||
p.removeConn(cn)
|
p.removeConn(cn)
|
||||||
p.connsMu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ConnPool) removeConn(cn *Conn) {
|
func (p *ConnPool) removeConn(cn *Conn) {
|
||||||
|
|
@ -395,15 +405,13 @@ func (p *ConnPool) removeConn(cn *Conn) {
|
||||||
p.poolSize--
|
p.poolSize--
|
||||||
p.checkMinIdleConns()
|
p.checkMinIdleConns()
|
||||||
}
|
}
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
atomic.AddUint32(&p.stats.StaleConns, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ConnPool) closeConn(cn *Conn) error {
|
func (p *ConnPool) closeConn(cn *Conn) error {
|
||||||
if p.opt.OnClose != nil {
|
|
||||||
_ = p.opt.OnClose(cn)
|
|
||||||
}
|
|
||||||
return cn.Close()
|
return cn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,14 +432,13 @@ func (p *ConnPool) IdleLen() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ConnPool) Stats() *Stats {
|
func (p *ConnPool) Stats() *Stats {
|
||||||
idleLen := p.IdleLen()
|
|
||||||
return &Stats{
|
return &Stats{
|
||||||
Hits: atomic.LoadUint32(&p.stats.Hits),
|
Hits: atomic.LoadUint32(&p.stats.Hits),
|
||||||
Misses: atomic.LoadUint32(&p.stats.Misses),
|
Misses: atomic.LoadUint32(&p.stats.Misses),
|
||||||
Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
|
Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
|
||||||
|
|
||||||
TotalConns: uint32(p.Len()),
|
TotalConns: uint32(p.Len()),
|
||||||
IdleConns: uint32(idleLen),
|
IdleConns: uint32(p.IdleLen()),
|
||||||
StaleConns: atomic.LoadUint32(&p.stats.StaleConns),
|
StaleConns: atomic.LoadUint32(&p.stats.StaleConns),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -459,7 +466,6 @@ func (p *ConnPool) Close() error {
|
||||||
if !atomic.CompareAndSwapUint32(&p._closed, 0, 1) {
|
if !atomic.CompareAndSwapUint32(&p._closed, 0, 1) {
|
||||||
return ErrClosed
|
return ErrClosed
|
||||||
}
|
}
|
||||||
close(p.closedCh)
|
|
||||||
|
|
||||||
var firstErr error
|
var firstErr error
|
||||||
p.connsMu.Lock()
|
p.connsMu.Lock()
|
||||||
|
|
@ -477,81 +483,20 @@ func (p *ConnPool) Close() error {
|
||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *ConnPool) reaper(frequency time.Duration) {
|
func (p *ConnPool) isHealthyConn(cn *Conn) bool {
|
||||||
ticker := time.NewTicker(frequency)
|
now := time.Now()
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
if p.cfg.ConnMaxLifetime > 0 && now.Sub(cn.createdAt) >= p.cfg.ConnMaxLifetime {
|
||||||
select {
|
return false
|
||||||
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.ConnMaxIdleTime > 0 && now.Sub(cn.UsedAt()) >= p.cfg.ConnMaxIdleTime {
|
||||||
|
|
||||||
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 {
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
if connCheck(cn.netConn) != nil {
|
||||||
if p.opt.IdleTimeout > 0 && now.Sub(cn.UsedAt()) >= p.opt.IdleTimeout {
|
return false
|
||||||
return true
|
|
||||||
}
|
|
||||||
if p.opt.MaxConnAge > 0 && now.Sub(cn.createdAt) >= p.opt.MaxConnAge {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
cn.SetUsedAt(now)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
@ -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'
|
||||||
|
}
|
||||||
|
|
@ -3,13 +3,15 @@ package proto
|
||||||
import (
|
import (
|
||||||
"encoding"
|
"encoding"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"time"
|
"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.
|
// Scan parses bytes `b` to `v` with appropriate type.
|
||||||
|
//
|
||||||
//nolint:gocyclo
|
//nolint:gocyclo
|
||||||
func Scan(b []byte, v interface{}) error {
|
func Scan(b []byte, v interface{}) error {
|
||||||
switch v := v.(type) {
|
switch v := v.(type) {
|
||||||
|
|
@ -115,6 +117,9 @@ func Scan(b []byte, v interface{}) error {
|
||||||
return nil
|
return nil
|
||||||
case encoding.BinaryUnmarshaler:
|
case encoding.BinaryUnmarshaler:
|
||||||
return v.UnmarshalBinary(b)
|
return v.UnmarshalBinary(b)
|
||||||
|
case *net.IP:
|
||||||
|
*v = b
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"redis: can't unmarshal %T (consider implementing BinaryUnmarshaler)", v)
|
"redis: can't unmarshal %T (consider implementing BinaryUnmarshaler)", v)
|
||||||
|
|
@ -4,16 +4,17 @@ import (
|
||||||
"encoding"
|
"encoding"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/util"
|
"github.com/redis/go-redis/v9/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type writer interface {
|
type writer interface {
|
||||||
io.Writer
|
io.Writer
|
||||||
io.ByteWriter
|
io.ByteWriter
|
||||||
// io.StringWriter
|
// WriteString implement io.StringWriter.
|
||||||
WriteString(s string) (n int, err error)
|
WriteString(s string) (n int, err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -34,7 +35,7 @@ func NewWriter(wr writer) *Writer {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Writer) WriteArgs(args []interface{}) error {
|
func (w *Writer) WriteArgs(args []interface{}) error {
|
||||||
if err := w.WriteByte(ArrayReply); err != nil {
|
if err := w.WriteByte(RespArray); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,6 +107,8 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return w.bytes(b)
|
return w.bytes(b)
|
||||||
|
case net.IP:
|
||||||
|
return w.bytes(v)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf(
|
return fmt.Errorf(
|
||||||
"redis: can't marshal %T (implement encoding.BinaryMarshaler)", v)
|
"redis: can't marshal %T (implement encoding.BinaryMarshaler)", v)
|
||||||
|
|
@ -113,7 +116,7 @@ func (w *Writer) WriteArg(v interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Writer) bytes(b []byte) error {
|
func (w *Writer) bytes(b []byte) error {
|
||||||
if err := w.WriteByte(StringReply); err != nil {
|
if err := w.WriteByte(RespString); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"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 {
|
func Sleep(ctx context.Context, dur time.Duration) error {
|
||||||
|
|
@ -2,30 +2,21 @@ package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ScanIterator is used to incrementally iterate over a collection of elements.
|
// ScanIterator is used to incrementally iterate over a collection of elements.
|
||||||
// It's safe for concurrent use by multiple goroutines.
|
|
||||||
type ScanIterator struct {
|
type ScanIterator struct {
|
||||||
mu sync.Mutex // protects Scanner and pos
|
|
||||||
cmd *ScanCmd
|
cmd *ScanCmd
|
||||||
pos int
|
pos int
|
||||||
}
|
}
|
||||||
|
|
||||||
// Err returns the last iterator error, if any.
|
// Err returns the last iterator error, if any.
|
||||||
func (it *ScanIterator) Err() error {
|
func (it *ScanIterator) Err() error {
|
||||||
it.mu.Lock()
|
return it.cmd.Err()
|
||||||
err := it.cmd.Err()
|
|
||||||
it.mu.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next advances the cursor and returns true if more values can be read.
|
// Next advances the cursor and returns true if more values can be read.
|
||||||
func (it *ScanIterator) Next(ctx context.Context) bool {
|
func (it *ScanIterator) Next(ctx context.Context) bool {
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
// Instantly return on errors.
|
// Instantly return on errors.
|
||||||
if it.cmd.Err() != nil {
|
if it.cmd.Err() != nil {
|
||||||
return false
|
return false
|
||||||
|
|
@ -68,10 +59,8 @@ func (it *ScanIterator) Next(ctx context.Context) bool {
|
||||||
// Val returns the key/field at the current cursor position.
|
// Val returns the key/field at the current cursor position.
|
||||||
func (it *ScanIterator) Val() string {
|
func (it *ScanIterator) Val() string {
|
||||||
var v string
|
var v string
|
||||||
it.mu.Lock()
|
|
||||||
if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) {
|
if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) {
|
||||||
v = it.cmd.page[it.pos-1]
|
v = it.cmd.page[it.pos-1]
|
||||||
}
|
}
|
||||||
it.mu.Unlock()
|
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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.
|
// Limiter is the interface of a rate limiter or a circuit breaker.
|
||||||
|
|
@ -27,7 +27,7 @@ type Limiter interface {
|
||||||
ReportResult(result error)
|
ReportResult(result error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Options keeps the settings to setup redis connection.
|
// Options keeps the settings to set up redis connection.
|
||||||
type Options struct {
|
type Options struct {
|
||||||
// The network type, either tcp or unix.
|
// The network type, either tcp or unix.
|
||||||
// Default is tcp.
|
// Default is tcp.
|
||||||
|
|
@ -35,6 +35,9 @@ type Options struct {
|
||||||
// host:port address.
|
// host:port address.
|
||||||
Addr string
|
Addr string
|
||||||
|
|
||||||
|
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
|
||||||
|
ClientName string
|
||||||
|
|
||||||
// Dialer creates new network connection and has priority over
|
// Dialer creates new network connection and has priority over
|
||||||
// Network and Addr options.
|
// Network and Addr options.
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
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.
|
// Hook that is called when new connection is established.
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
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
|
// Use the specified Username to authenticate the current connection
|
||||||
// with one of the connections defined in the ACL list when connecting
|
// 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.
|
// 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,
|
// or the User Password when connecting to a Redis 6.0 instance, or greater,
|
||||||
// that is using the Redis ACL system.
|
// that is using the Redis ACL system.
|
||||||
Password string
|
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.
|
// Database to be selected after connecting to the server.
|
||||||
DB int
|
DB int
|
||||||
|
|
@ -69,49 +78,64 @@ type Options struct {
|
||||||
// Default is 5 seconds.
|
// Default is 5 seconds.
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
// Timeout for socket reads. If reached, commands will fail
|
// 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.
|
// with a timeout instead of blocking. Supported values:
|
||||||
// Default is 3 seconds.
|
// - `0` - default timeout (3 seconds).
|
||||||
|
// - `-1` - no timeout (block indefinitely).
|
||||||
|
// - `-2` - disables SetReadDeadline calls completely.
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
// Timeout for socket writes. If reached, commands will fail
|
// Timeout for socket writes. If reached, commands will fail
|
||||||
// with a timeout instead of blocking.
|
// with a timeout instead of blocking. Supported values:
|
||||||
// Default is ReadTimeout.
|
// - `0` - default timeout (3 seconds).
|
||||||
|
// - `-1` - no timeout (block indefinitely).
|
||||||
|
// - `-2` - disables SetWriteDeadline calls completely.
|
||||||
WriteTimeout time.Duration
|
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.
|
// Type of connection pool.
|
||||||
// true for FIFO pool, false for LIFO 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
|
PoolFIFO bool
|
||||||
// Maximum number of socket connections.
|
// Maximum number of socket connections.
|
||||||
// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
||||||
PoolSize int
|
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
|
// Amount of time client waits for connection if all connections
|
||||||
// are busy before returning an error.
|
// are busy before returning an error.
|
||||||
// Default is ReadTimeout + 1 second.
|
// Default is ReadTimeout + 1 second.
|
||||||
PoolTimeout time.Duration
|
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
|
||||||
|
// ConnMaxIdleTime is the maximum amount of time a connection may be idle.
|
||||||
// Should be less than server's timeout.
|
// Should be less than server's timeout.
|
||||||
// Default is 5 minutes. -1 disables idle timeout check.
|
//
|
||||||
IdleTimeout time.Duration
|
// Expired connections may be closed lazily before reuse.
|
||||||
// Frequency of idle checks made by idle connections reaper.
|
// If d <= 0, connections are not closed due to a connection's idle time.
|
||||||
// Default is 1 minute. -1 disables idle connections reaper,
|
//
|
||||||
// but idle connections are still discarded by the client
|
// Default is 30 minutes. -1 disables idle timeout check.
|
||||||
// if IdleTimeout is set.
|
ConnMaxIdleTime time.Duration
|
||||||
IdleCheckFrequency 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.
|
// TLS Config to use. When set, TLS will be negotiated.
|
||||||
readOnly bool
|
|
||||||
|
|
||||||
// TLS Config to use. When set TLS will be negotiated.
|
|
||||||
TLSConfig *tls.Config
|
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
|
Limiter Limiter
|
||||||
|
|
||||||
|
// Enables read only queries on slave/follower nodes.
|
||||||
|
readOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opt *Options) init() {
|
func (opt *Options) init() {
|
||||||
|
|
@ -129,40 +153,36 @@ func (opt *Options) init() {
|
||||||
opt.DialTimeout = 5 * time.Second
|
opt.DialTimeout = 5 * time.Second
|
||||||
}
|
}
|
||||||
if opt.Dialer == nil {
|
if opt.Dialer == nil {
|
||||||
opt.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
opt.Dialer = NewDialer(opt)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if opt.PoolSize == 0 {
|
if opt.PoolSize == 0 {
|
||||||
opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
|
opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
|
||||||
}
|
}
|
||||||
switch opt.ReadTimeout {
|
switch opt.ReadTimeout {
|
||||||
|
case -2:
|
||||||
|
opt.ReadTimeout = -1
|
||||||
case -1:
|
case -1:
|
||||||
opt.ReadTimeout = 0
|
opt.ReadTimeout = 0
|
||||||
case 0:
|
case 0:
|
||||||
opt.ReadTimeout = 3 * time.Second
|
opt.ReadTimeout = 3 * time.Second
|
||||||
}
|
}
|
||||||
switch opt.WriteTimeout {
|
switch opt.WriteTimeout {
|
||||||
|
case -2:
|
||||||
|
opt.WriteTimeout = -1
|
||||||
case -1:
|
case -1:
|
||||||
opt.WriteTimeout = 0
|
opt.WriteTimeout = 0
|
||||||
case 0:
|
case 0:
|
||||||
opt.WriteTimeout = opt.ReadTimeout
|
opt.WriteTimeout = opt.ReadTimeout
|
||||||
}
|
}
|
||||||
if opt.PoolTimeout == 0 {
|
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 {
|
if opt.ConnMaxIdleTime == 0 {
|
||||||
opt.IdleTimeout = 5 * time.Minute
|
opt.ConnMaxIdleTime = 30 * time.Minute
|
||||||
}
|
|
||||||
if opt.IdleCheckFrequency == 0 {
|
|
||||||
opt.IdleCheckFrequency = time.Minute
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opt.MaxRetries == -1 {
|
if opt.MaxRetries == -1 {
|
||||||
|
|
@ -189,36 +209,57 @@ func (opt *Options) clone() *Options {
|
||||||
return &clone
|
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.
|
// ParseURL parses an URL into Options that can be used to connect to Redis.
|
||||||
// Scheme is required.
|
// Scheme is required.
|
||||||
// There are two connection types: by tcp socket and by unix socket.
|
// There are two connection types: by tcp socket and by unix socket.
|
||||||
// Tcp connection:
|
// Tcp connection:
|
||||||
// redis://<user>:<password>@<host>:<port>/<db_number>
|
//
|
||||||
|
// redis://<user>:<password>@<host>:<port>/<db_number>
|
||||||
|
//
|
||||||
// Unix connection:
|
// 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:
|
// 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
|
// - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
|
||||||
// - only scalar type fields are supported (bool, int, time.Duration)
|
// - only scalar type fields are supported (bool, int, time.Duration)
|
||||||
// - for time.Duration fields, values must be a valid input for time.ParseDuration();
|
// - 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
|
// 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
|
// - 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
|
// value, leave the value blank or remove the parameter
|
||||||
// - only the last value is interpreted if a parameter is given multiple times
|
// - 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
|
// - fields "network", "addr", "username" and "password" can only be set using other
|
||||||
// URL attributes (scheme, host, userinfo, resp.), query paremeters using these
|
// URL attributes (scheme, host, userinfo, resp.), query paremeters using these
|
||||||
// names will be treated as unknown parameters
|
// names will be treated as unknown parameters
|
||||||
// - unknown parameter names will result in an error
|
// - unknown parameter names will result in an error
|
||||||
|
//
|
||||||
// Examples:
|
// Examples:
|
||||||
// redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
|
//
|
||||||
// is equivalent to:
|
// redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
|
||||||
// &Options{
|
// is equivalent to:
|
||||||
// Network: "tcp",
|
// &Options{
|
||||||
// Addr: "localhost:6789",
|
// Network: "tcp",
|
||||||
// DB: 1, // path "/3" was overridden by "&db=1"
|
// Addr: "localhost:6789",
|
||||||
// DialTimeout: 3 * time.Second, // no time unit = seconds
|
// DB: 1, // path "/3" was overridden by "&db=1"
|
||||||
// ReadTimeout: 6 * time.Second,
|
// DialTimeout: 3 * time.Second, // no time unit = seconds
|
||||||
// MaxRetries: 2,
|
// ReadTimeout: 6 * time.Second,
|
||||||
// }
|
// MaxRetries: 2,
|
||||||
|
// }
|
||||||
func ParseURL(redisURL string) (*Options, error) {
|
func ParseURL(redisURL string) (*Options, error) {
|
||||||
u, err := url.Parse(redisURL)
|
u, err := url.Parse(redisURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -240,16 +281,7 @@ func setupTCPConn(u *url.URL) (*Options, error) {
|
||||||
|
|
||||||
o.Username, o.Password = getUserPassword(u)
|
o.Username, o.Password = getUserPassword(u)
|
||||||
|
|
||||||
h, p, err := net.SplitHostPort(u.Host)
|
h, p := getHostPortWithDefaults(u)
|
||||||
if err != nil {
|
|
||||||
h = u.Host
|
|
||||||
}
|
|
||||||
if h == "" {
|
|
||||||
h = "localhost"
|
|
||||||
}
|
|
||||||
if p == "" {
|
|
||||||
p = "6379"
|
|
||||||
}
|
|
||||||
o.Addr = net.JoinHostPort(h, p)
|
o.Addr = net.JoinHostPort(h, p)
|
||||||
|
|
||||||
f := strings.FieldsFunc(u.Path, func(r rune) bool {
|
f := strings.FieldsFunc(u.Path, func(r rune) bool {
|
||||||
|
|
@ -259,6 +291,7 @@ func setupTCPConn(u *url.URL) (*Options, error) {
|
||||||
case 0:
|
case 0:
|
||||||
o.DB = 0
|
o.DB = 0
|
||||||
case 1:
|
case 1:
|
||||||
|
var err error
|
||||||
if o.DB, err = strconv.Atoi(f[0]); err != nil {
|
if o.DB, err = strconv.Atoi(f[0]); err != nil {
|
||||||
return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
|
return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
|
||||||
}
|
}
|
||||||
|
|
@ -267,12 +300,32 @@ func setupTCPConn(u *url.URL) (*Options, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.Scheme == "rediss" {
|
if u.Scheme == "rediss" {
|
||||||
o.TLSConfig = &tls.Config{ServerName: h}
|
o.TLSConfig = &tls.Config{
|
||||||
|
ServerName: h,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return setupConnParams(u, o)
|
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) {
|
func setupUnixConn(u *url.URL) (*Options, error) {
|
||||||
o := &Options{
|
o := &Options{
|
||||||
Network: "unix",
|
Network: "unix",
|
||||||
|
|
@ -291,6 +344,10 @@ type queryOptions struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *queryOptions) has(name string) bool {
|
||||||
|
return len(o.q[name]) > 0
|
||||||
|
}
|
||||||
|
|
||||||
func (o *queryOptions) string(name string) string {
|
func (o *queryOptions) string(name string) string {
|
||||||
vs := o.q[name]
|
vs := o.q[name]
|
||||||
if len(vs) == 0 {
|
if len(vs) == 0 {
|
||||||
|
|
@ -300,6 +357,12 @@ func (o *queryOptions) string(name string) string {
|
||||||
return vs[len(vs)-1]
|
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 {
|
func (o *queryOptions) int(name string) int {
|
||||||
s := o.string(name)
|
s := o.string(name)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
|
|
@ -377,6 +440,8 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) {
|
||||||
o.DB = db
|
o.DB = db
|
||||||
}
|
}
|
||||||
|
|
||||||
|
o.Protocol = q.int("protocol")
|
||||||
|
o.ClientName = q.string("client_name")
|
||||||
o.MaxRetries = q.int("max_retries")
|
o.MaxRetries = q.int("max_retries")
|
||||||
o.MinRetryBackoff = q.duration("min_retry_backoff")
|
o.MinRetryBackoff = q.duration("min_retry_backoff")
|
||||||
o.MaxRetryBackoff = q.duration("max_retry_backoff")
|
o.MaxRetryBackoff = q.duration("max_retry_backoff")
|
||||||
|
|
@ -385,11 +450,19 @@ func setupConnParams(u *url.URL, o *Options) (*Options, error) {
|
||||||
o.WriteTimeout = q.duration("write_timeout")
|
o.WriteTimeout = q.duration("write_timeout")
|
||||||
o.PoolFIFO = q.bool("pool_fifo")
|
o.PoolFIFO = q.bool("pool_fifo")
|
||||||
o.PoolSize = q.int("pool_size")
|
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.PoolTimeout = q.duration("pool_timeout")
|
||||||
o.IdleTimeout = q.duration("idle_timeout")
|
o.MinIdleConns = q.int("min_idle_conns")
|
||||||
o.IdleCheckFrequency = q.duration("idle_check_frequency")
|
o.MaxIdleConns = q.int("max_idle_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 {
|
if q.err != nil {
|
||||||
return nil, q.err
|
return nil, q.err
|
||||||
}
|
}
|
||||||
|
|
@ -413,17 +486,20 @@ func getUserPassword(u *url.URL) (string, string) {
|
||||||
return user, password
|
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{
|
return pool.NewConnPool(&pool.Options{
|
||||||
Dialer: func(ctx context.Context) (net.Conn, error) {
|
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,
|
PoolFIFO: opt.PoolFIFO,
|
||||||
PoolSize: opt.PoolSize,
|
PoolSize: opt.PoolSize,
|
||||||
MinIdleConns: opt.MinIdleConns,
|
PoolTimeout: opt.PoolTimeout,
|
||||||
MaxConnAge: opt.MaxConnAge,
|
MinIdleConns: opt.MinIdleConns,
|
||||||
PoolTimeout: opt.PoolTimeout,
|
MaxIdleConns: opt.MaxIdleConns,
|
||||||
IdleTimeout: opt.IdleTimeout,
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "redis",
|
"name": "redis",
|
||||||
"version": "8.11.5",
|
"version": "9.1.0",
|
||||||
"main": "index.js",
|
"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>",
|
"author": "Vladimir Mihailenco <vladimir.webdev@gmail.com>",
|
||||||
"license": "BSD-2-clause"
|
"license": "BSD-2-clause"
|
||||||
}
|
}
|
||||||
|
|
@ -2,9 +2,7 @@ package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"errors"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type pipelineExecer func(context.Context, []Cmder) error
|
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
|
// 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
|
// 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
|
// See https://redis.io/topics/pipelining
|
||||||
//
|
//
|
||||||
// Pay attention, that Pipeline is not a transaction, so you can get unexpected
|
// 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.
|
// depends of your batch size and/or use TxPipeline.
|
||||||
type Pipeliner interface {
|
type Pipeliner interface {
|
||||||
StatefulCmdable
|
StatefulCmdable
|
||||||
|
|
||||||
|
// Len is to obtain the number of commands in the pipeline that have not yet been executed.
|
||||||
Len() int
|
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
|
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
|
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)
|
Exec(ctx context.Context) ([]Cmder, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Pipeliner = (*Pipeline)(nil)
|
var _ Pipeliner = (*Pipeline)(nil)
|
||||||
|
|
||||||
// Pipeline implements pipelining as described in
|
// Pipeline implements pipelining as described in
|
||||||
// http://redis.io/topics/pipelining. It's safe for concurrent use
|
// http://redis.io/topics/pipelining.
|
||||||
// by multiple goroutines.
|
// Please note: it is not safe for concurrent use by multiple goroutines.
|
||||||
type Pipeline struct {
|
type Pipeline struct {
|
||||||
cmdable
|
cmdable
|
||||||
statefulCmdable
|
statefulCmdable
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
exec pipelineExecer
|
exec pipelineExecer
|
||||||
|
cmds []Cmder
|
||||||
mu sync.Mutex
|
|
||||||
cmds []Cmder
|
|
||||||
closed bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Pipeline) init() {
|
func (c *Pipeline) init() {
|
||||||
|
|
@ -56,50 +60,29 @@ func (c *Pipeline) init() {
|
||||||
|
|
||||||
// Len returns the number of queued commands.
|
// Len returns the number of queued commands.
|
||||||
func (c *Pipeline) Len() int {
|
func (c *Pipeline) Len() int {
|
||||||
c.mu.Lock()
|
return len(c.cmds)
|
||||||
ln := len(c.cmds)
|
|
||||||
c.mu.Unlock()
|
|
||||||
return ln
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do queues the custom command for later execution.
|
// Do queues the custom command for later execution.
|
||||||
func (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd {
|
func (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||||
cmd := NewCmd(ctx, args...)
|
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)
|
_ = c.Process(ctx, cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process queues the cmd for later execution.
|
// Process queues the cmd for later execution.
|
||||||
func (c *Pipeline) Process(ctx context.Context, cmd Cmder) error {
|
func (c *Pipeline) Process(ctx context.Context, cmd Cmder) error {
|
||||||
c.mu.Lock()
|
|
||||||
c.cmds = append(c.cmds, cmd)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discard resets the pipeline and discards queued commands.
|
// Discard resets the pipeline and discards queued commands.
|
||||||
func (c *Pipeline) Discard() error {
|
func (c *Pipeline) Discard() {
|
||||||
c.mu.Lock()
|
|
||||||
err := c.discard()
|
|
||||||
c.mu.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Pipeline) discard() error {
|
|
||||||
if c.closed {
|
|
||||||
return pool.ErrClosed
|
|
||||||
}
|
|
||||||
c.cmds = c.cmds[:0]
|
c.cmds = c.cmds[:0]
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exec executes all previously queued commands using one
|
// 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
|
// Exec always returns list of commands and error of the first failed
|
||||||
// command if any.
|
// command if any.
|
||||||
func (c *Pipeline) Exec(ctx context.Context) ([]Cmder, error) {
|
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 {
|
if len(c.cmds) == 0 {
|
||||||
return nil, nil
|
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 {
|
if err := fn(c); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
cmds, err := c.Exec(ctx)
|
return c.Exec(ctx)
|
||||||
_ = c.Close()
|
|
||||||
return cmds, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Pipeline) Pipeline() Pipeliner {
|
func (c *Pipeline) Pipeline() Pipeliner {
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -7,9 +7,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
"github.com/redis/go-redis/v9/internal"
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
"github.com/redis/go-redis/v9/internal/pool"
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
"github.com/redis/go-redis/v9/internal/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PubSub implements Pub/Sub commands as described in
|
// 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)
|
newConn func(ctx context.Context, channels []string) (*pool.Conn, error)
|
||||||
closeConn func(*pool.Conn) error
|
closeConn func(*pool.Conn) error
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
cn *pool.Conn
|
cn *pool.Conn
|
||||||
channels map[string]struct{}
|
channels map[string]struct{}
|
||||||
patterns map[string]struct{}
|
patterns map[string]struct{}
|
||||||
|
schannels map[string]struct{}
|
||||||
|
|
||||||
closed bool
|
closed bool
|
||||||
exit chan struct{}
|
exit chan struct{}
|
||||||
|
|
@ -46,6 +47,7 @@ func (c *PubSub) init() {
|
||||||
func (c *PubSub) String() string {
|
func (c *PubSub) String() string {
|
||||||
channels := mapKeys(c.channels)
|
channels := mapKeys(c.channels)
|
||||||
channels = append(channels, mapKeys(c.patterns)...)
|
channels = append(channels, mapKeys(c.patterns)...)
|
||||||
|
channels = append(channels, mapKeys(c.schannels)...)
|
||||||
return fmt.Sprintf("PubSub(%s)", strings.Join(channels, ", "))
|
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 {
|
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)
|
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
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,15 +217,38 @@ func (c *PubSub) PSubscribe(ctx context.Context, patterns ...string) error {
|
||||||
return err
|
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
|
// Unsubscribe the client from the given channels, or from all of
|
||||||
// them if none is given.
|
// them if none is given.
|
||||||
func (c *PubSub) Unsubscribe(ctx context.Context, channels ...string) error {
|
func (c *PubSub) Unsubscribe(ctx context.Context, channels ...string) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
for _, channel := range channels {
|
if len(channels) > 0 {
|
||||||
delete(c.channels, channel)
|
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...)
|
err := c.subscribe(ctx, "unsubscribe", channels...)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -227,13 +259,42 @@ func (c *PubSub) PUnsubscribe(ctx context.Context, patterns ...string) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
if len(patterns) > 0 {
|
||||||
delete(c.patterns, pattern)
|
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...)
|
err := c.subscribe(ctx, "punsubscribe", patterns...)
|
||||||
return err
|
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 {
|
func (c *PubSub) subscribe(ctx context.Context, redisCmd string, channels ...string) error {
|
||||||
cn, err := c.conn(ctx, channels)
|
cn, err := c.conn(ctx, channels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -311,7 +372,7 @@ func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
|
||||||
}, nil
|
}, nil
|
||||||
case []interface{}:
|
case []interface{}:
|
||||||
switch kind := reply[0].(string); kind {
|
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".
|
// Can be nil in case of "unsubscribe".
|
||||||
channel, _ := reply[1].(string)
|
channel, _ := reply[1].(string)
|
||||||
return &Subscription{
|
return &Subscription{
|
||||||
|
|
@ -319,7 +380,7 @@ func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
|
||||||
Channel: channel,
|
Channel: channel,
|
||||||
Count: int(reply[2].(int64)),
|
Count: int(reply[2].(int64)),
|
||||||
}, nil
|
}, nil
|
||||||
case "message":
|
case "message", "smessage":
|
||||||
switch payload := reply[2].(type) {
|
switch payload := reply[2].(type) {
|
||||||
case string:
|
case string:
|
||||||
return &Message{
|
return &Message{
|
||||||
|
|
@ -371,7 +432,7 @@ func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (int
|
||||||
return nil, err
|
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)
|
return c.cmd.readReply(rd)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -456,9 +517,9 @@ func (c *PubSub) ChannelSize(size int) <-chan *Message {
|
||||||
// reconnections.
|
// reconnections.
|
||||||
//
|
//
|
||||||
// ChannelWithSubscriptions can not be used together with Channel or ChannelSize.
|
// 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.chOnce.Do(func() {
|
||||||
c.allCh = newChannel(c, WithChannelSize(size))
|
c.allCh = newChannel(c, opts...)
|
||||||
c.allCh.initAllChan()
|
c.allCh.initAllChan()
|
||||||
})
|
})
|
||||||
if c.allCh == nil {
|
if c.allCh == nil {
|
||||||
|
|
@ -0,0 +1,827 @@
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"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 {
|
||||||
|
slice []Hook
|
||||||
|
initial hooks
|
||||||
|
current hooks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hs *hooksMixin) initHooks(hooks hooks) {
|
||||||
|
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.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 {
|
||||||
|
clone := *hs
|
||||||
|
l := len(clone.slice)
|
||||||
|
clone.slice = clone.slice[:l:l]
|
||||||
|
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) {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
_, 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
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,161 @@
|
||||||
|
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 {
|
||||||
|
if options.Keys != nil {
|
||||||
|
for _, key := range options.Keys {
|
||||||
|
|
||||||
|
args = append(args, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Arguments != nil {
|
||||||
|
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 {
|
||||||
|
if options.Keys != nil {
|
||||||
|
for _, key := range options.Keys {
|
||||||
|
|
||||||
|
args = append(args, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Arguments != nil {
|
||||||
|
for _, key := range options.Arguments {
|
||||||
|
|
||||||
|
args = append(args, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd := NewCmd(ctx, args...)
|
||||||
|
_ = c(ctx, cmd)
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
|
@ -82,17 +82,17 @@ func NewBoolSliceResult(val []bool, err error) *BoolSliceCmd {
|
||||||
return &cmd
|
return &cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStringStringMapResult returns a StringStringMapCmd initialised with val and err for testing.
|
// NewMapStringStringResult returns a MapStringStringCmd initialised with val and err for testing.
|
||||||
func NewStringStringMapResult(val map[string]string, err error) *StringStringMapCmd {
|
func NewMapStringStringResult(val map[string]string, err error) *MapStringStringCmd {
|
||||||
var cmd StringStringMapCmd
|
var cmd MapStringStringCmd
|
||||||
cmd.val = val
|
cmd.val = val
|
||||||
cmd.SetErr(err)
|
cmd.SetErr(err)
|
||||||
return &cmd
|
return &cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStringIntMapCmdResult returns a StringIntMapCmd initialised with val and err for testing.
|
// NewMapStringIntCmdResult returns a MapStringIntCmd initialised with val and err for testing.
|
||||||
func NewStringIntMapCmdResult(val map[string]int64, err error) *StringIntMapCmd {
|
func NewMapStringIntCmdResult(val map[string]int64, err error) *MapStringIntCmd {
|
||||||
var cmd StringIntMapCmd
|
var cmd MapStringIntCmd
|
||||||
cmd.val = val
|
cmd.val = val
|
||||||
cmd.SetErr(err)
|
cmd.SetErr(err)
|
||||||
return &cmd
|
return &cmd
|
||||||
|
|
@ -114,7 +114,7 @@ func NewZSliceCmdResult(val []Z, err error) *ZSliceCmd {
|
||||||
return &cmd
|
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 {
|
func NewZWithKeyCmdResult(val *ZWithKey, err error) *ZWithKeyCmd {
|
||||||
var cmd ZWithKeyCmd
|
var cmd ZWithKeyCmd
|
||||||
cmd.val = val
|
cmd.val = val
|
||||||
|
|
@ -178,3 +178,11 @@ func NewXStreamSliceCmdResult(val []XStream, err error) *XStreamSliceCmd {
|
||||||
cmd.SetErr(err)
|
cmd.SetErr(err)
|
||||||
return &cmd
|
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
|
||||||
|
}
|
||||||
444
vendor/github.com/go-redis/redis/v8/ring.go → vendor/github.com/redis/go-redis/v9/ring.go
generated
vendored
444
vendor/github.com/go-redis/redis/v8/ring.go → vendor/github.com/redis/go-redis/v9/ring.go
generated
vendored
|
|
@ -12,12 +12,12 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/cespare/xxhash/v2"
|
"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/redis/go-redis/v9/internal"
|
||||||
"github.com/go-redis/redis/v8/internal/hashtag"
|
"github.com/redis/go-redis/v9/internal/hashtag"
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
"github.com/redis/go-redis/v9/internal/pool"
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
"github.com/redis/go-redis/v9/internal/rand"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errRingShardsDown = errors.New("redis: all ring shards are down")
|
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.
|
// Map of name => host:port addresses of ring shards.
|
||||||
Addrs map[string]string
|
Addrs map[string]string
|
||||||
|
|
||||||
// NewClient creates a shard client with provided name and options.
|
// NewClient creates a shard client with provided options.
|
||||||
NewClient func(name string, opt *Options) *Client
|
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.
|
// Frequency of PING commands sent to check shards availability.
|
||||||
// Shard is considered down after 3 subsequent failed checks.
|
// 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)
|
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
OnConnect func(ctx context.Context, cn *Conn) error
|
||||||
|
|
||||||
|
Protocol int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
DB int
|
DB int
|
||||||
|
|
@ -82,12 +86,12 @@ type RingOptions struct {
|
||||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
||||||
PoolFIFO bool
|
PoolFIFO bool
|
||||||
|
|
||||||
PoolSize int
|
PoolSize int
|
||||||
MinIdleConns int
|
PoolTimeout time.Duration
|
||||||
MaxConnAge time.Duration
|
MinIdleConns int
|
||||||
PoolTimeout time.Duration
|
MaxIdleConns int
|
||||||
IdleTimeout time.Duration
|
ConnMaxIdleTime time.Duration
|
||||||
IdleCheckFrequency time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
Limiter Limiter
|
Limiter Limiter
|
||||||
|
|
@ -95,7 +99,7 @@ type RingOptions struct {
|
||||||
|
|
||||||
func (opt *RingOptions) init() {
|
func (opt *RingOptions) init() {
|
||||||
if opt.NewClient == nil {
|
if opt.NewClient == nil {
|
||||||
opt.NewClient = func(name string, opt *Options) *Client {
|
opt.NewClient = func(opt *Options) *Client {
|
||||||
return NewClient(opt)
|
return NewClient(opt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,9 +133,11 @@ func (opt *RingOptions) init() {
|
||||||
|
|
||||||
func (opt *RingOptions) clientOptions() *Options {
|
func (opt *RingOptions) clientOptions() *Options {
|
||||||
return &Options{
|
return &Options{
|
||||||
Dialer: opt.Dialer,
|
ClientName: opt.ClientName,
|
||||||
OnConnect: opt.OnConnect,
|
Dialer: opt.Dialer,
|
||||||
|
OnConnect: opt.OnConnect,
|
||||||
|
|
||||||
|
Protocol: opt.Protocol,
|
||||||
Username: opt.Username,
|
Username: opt.Username,
|
||||||
Password: opt.Password,
|
Password: opt.Password,
|
||||||
DB: opt.DB,
|
DB: opt.DB,
|
||||||
|
|
@ -142,13 +148,13 @@ func (opt *RingOptions) clientOptions() *Options {
|
||||||
ReadTimeout: opt.ReadTimeout,
|
ReadTimeout: opt.ReadTimeout,
|
||||||
WriteTimeout: opt.WriteTimeout,
|
WriteTimeout: opt.WriteTimeout,
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
PoolFIFO: opt.PoolFIFO,
|
||||||
PoolSize: opt.PoolSize,
|
PoolSize: opt.PoolSize,
|
||||||
MinIdleConns: opt.MinIdleConns,
|
PoolTimeout: opt.PoolTimeout,
|
||||||
MaxConnAge: opt.MaxConnAge,
|
MinIdleConns: opt.MinIdleConns,
|
||||||
PoolTimeout: opt.PoolTimeout,
|
MaxIdleConns: opt.MaxIdleConns,
|
||||||
IdleTimeout: opt.IdleTimeout,
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
TLSConfig: opt.TLSConfig,
|
||||||
Limiter: opt.Limiter,
|
Limiter: opt.Limiter,
|
||||||
|
|
@ -160,14 +166,16 @@ func (opt *RingOptions) clientOptions() *Options {
|
||||||
type ringShard struct {
|
type ringShard struct {
|
||||||
Client *Client
|
Client *Client
|
||||||
down int32
|
down int32
|
||||||
|
addr string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRingShard(opt *RingOptions, name, addr string) *ringShard {
|
func newRingShard(opt *RingOptions, addr string) *ringShard {
|
||||||
clopt := opt.clientOptions()
|
clopt := opt.clientOptions()
|
||||||
clopt.Addr = addr
|
clopt.Addr = addr
|
||||||
|
|
||||||
return &ringShard{
|
return &ringShard{
|
||||||
Client: opt.NewClient(name, clopt),
|
Client: opt.NewClient(clopt),
|
||||||
|
addr: addr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -208,161 +216,238 @@ func (shard *ringShard) Vote(up bool) bool {
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
type ringShards struct {
|
type ringSharding struct {
|
||||||
opt *RingOptions
|
opt *RingOptions
|
||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
hash ConsistentHash
|
shards *ringShards
|
||||||
shards map[string]*ringShard // read only
|
closed bool
|
||||||
list []*ringShard // read only
|
hash ConsistentHash
|
||||||
numShard int
|
numShard int
|
||||||
closed bool
|
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 {
|
type ringShards struct {
|
||||||
shards := make(map[string]*ringShard, len(opt.Addrs))
|
m map[string]*ringShard
|
||||||
list := make([]*ringShard, 0, len(shards))
|
list []*ringShard
|
||||||
|
}
|
||||||
|
|
||||||
for name, addr := range opt.Addrs {
|
func newRingSharding(opt *RingOptions) *ringSharding {
|
||||||
shard := newRingShard(opt, name, addr)
|
c := &ringSharding{
|
||||||
shards[name] = shard
|
|
||||||
|
|
||||||
list = append(list, shard)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &ringShards{
|
|
||||||
opt: opt,
|
opt: opt,
|
||||||
|
|
||||||
shards: shards,
|
|
||||||
list: list,
|
|
||||||
}
|
}
|
||||||
c.rebalance()
|
c.SetAddrs(opt.Addrs)
|
||||||
|
|
||||||
return c
|
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
|
var list []*ringShard
|
||||||
|
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
if !c.closed {
|
if !c.closed {
|
||||||
list = c.list
|
list = c.shards.list
|
||||||
}
|
}
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
|
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ringShards) Hash(key string) string {
|
func (c *ringSharding) Hash(key string) string {
|
||||||
key = hashtag.Key(key)
|
key = hashtag.Key(key)
|
||||||
|
|
||||||
var hash string
|
var hash string
|
||||||
|
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
if c.numShard > 0 {
|
if c.numShard > 0 {
|
||||||
hash = c.hash.Get(key)
|
hash = c.hash.Get(key)
|
||||||
}
|
}
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
return hash
|
return hash
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ringShards) GetByKey(key string) (*ringShard, error) {
|
func (c *ringSharding) GetByKey(key string) (*ringShard, error) {
|
||||||
key = hashtag.Key(key)
|
key = hashtag.Key(key)
|
||||||
|
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
if c.closed {
|
if c.closed {
|
||||||
c.mu.RUnlock()
|
|
||||||
return nil, pool.ErrClosed
|
return nil, pool.ErrClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.numShard == 0 {
|
if c.numShard == 0 {
|
||||||
c.mu.RUnlock()
|
|
||||||
return nil, errRingShardsDown
|
return nil, errRingShardsDown
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := c.hash.Get(key)
|
shardName := c.hash.Get(key)
|
||||||
if hash == "" {
|
if shardName == "" {
|
||||||
c.mu.RUnlock()
|
|
||||||
return nil, errRingShardsDown
|
return nil, errRingShardsDown
|
||||||
}
|
}
|
||||||
|
return c.shards.m[shardName], nil
|
||||||
shard := c.shards[hash]
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
return shard, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ringShards) GetByName(shardName string) (*ringShard, error) {
|
func (c *ringSharding) GetByName(shardName string) (*ringShard, error) {
|
||||||
if shardName == "" {
|
if shardName == "" {
|
||||||
return c.Random()
|
return c.Random()
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
shard := c.shards[shardName]
|
defer c.mu.RUnlock()
|
||||||
c.mu.RUnlock()
|
|
||||||
return shard, nil
|
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()))
|
return c.GetByKey(strconv.Itoa(rand.Int()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// heartbeat monitors state of each shard in the ring.
|
// Heartbeat monitors state of each shard in the ring.
|
||||||
func (c *ringShards) Heartbeat(frequency time.Duration) {
|
func (c *ringSharding) Heartbeat(ctx context.Context, frequency time.Duration) {
|
||||||
ticker := time.NewTicker(frequency)
|
ticker := time.NewTicker(frequency)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
ctx := context.Background()
|
for {
|
||||||
for range ticker.C {
|
select {
|
||||||
var rebalance bool
|
case <-ticker.C:
|
||||||
|
var rebalance bool
|
||||||
|
|
||||||
for _, shard := range c.List() {
|
for _, shard := range c.List() {
|
||||||
err := shard.Client.Ping(ctx).Err()
|
err := shard.Client.Ping(ctx).Err()
|
||||||
isUp := err == nil || err == pool.ErrPoolTimeout
|
isUp := err == nil || err == pool.ErrPoolTimeout
|
||||||
if shard.Vote(isUp) {
|
if shard.Vote(isUp) {
|
||||||
internal.Logger.Printf(context.Background(), "ring shard state changed: %s", shard)
|
internal.Logger.Printf(ctx, "ring shard state changed: %s", shard)
|
||||||
rebalance = true
|
rebalance = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if rebalance {
|
if rebalance {
|
||||||
c.rebalance()
|
c.mu.Lock()
|
||||||
|
c.rebalanceLocked()
|
||||||
|
c.mu.Unlock()
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// rebalance removes dead shards from the Ring.
|
// rebalanceLocked removes dead shards from the Ring.
|
||||||
func (c *ringShards) rebalance() {
|
// Requires c.mu locked.
|
||||||
c.mu.RLock()
|
func (c *ringSharding) rebalanceLocked() {
|
||||||
shards := c.shards
|
if c.closed {
|
||||||
c.mu.RUnlock()
|
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() {
|
if shard.IsUp() {
|
||||||
liveShards = append(liveShards, name)
|
liveShards = append(liveShards, name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hash := c.opt.NewConsistentHash(liveShards)
|
c.hash = c.opt.NewConsistentHash(liveShards)
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
c.hash = hash
|
|
||||||
c.numShard = len(liveShards)
|
c.numShard = len(liveShards)
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ringShards) Len() int {
|
func (c *ringSharding) Len() int {
|
||||||
c.mu.RLock()
|
c.mu.RLock()
|
||||||
l := c.numShard
|
defer c.mu.RUnlock()
|
||||||
c.mu.RUnlock()
|
|
||||||
return l
|
return c.numShard
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ringShards) Close() error {
|
func (c *ringSharding) Close() error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
|
|
@ -372,26 +457,22 @@ func (c *ringShards) Close() error {
|
||||||
c.closed = true
|
c.closed = true
|
||||||
|
|
||||||
var firstErr error
|
var firstErr error
|
||||||
for _, shard := range c.shards {
|
|
||||||
|
for _, shard := range c.shards.list {
|
||||||
if err := shard.Client.Close(); err != nil && firstErr == nil {
|
if err := shard.Client.Close(); err != nil && firstErr == nil {
|
||||||
firstErr = err
|
firstErr = err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.hash = nil
|
c.hash = nil
|
||||||
c.shards = nil
|
c.shards = nil
|
||||||
c.list = nil
|
c.numShard = 0
|
||||||
|
|
||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
type ring struct {
|
|
||||||
opt *RingOptions
|
|
||||||
shards *ringShards
|
|
||||||
cmdsInfoCache *cmdsInfoCache //nolint:structcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ring is a Redis client that uses consistent hashing to distribute
|
// Ring is a Redis client that uses consistent hashing to distribute
|
||||||
// keys across multiple Redis servers (shards). It's safe for
|
// keys across multiple Redis servers (shards). It's safe for
|
||||||
// concurrent use by multiple goroutines.
|
// concurrent use by multiple goroutines.
|
||||||
|
|
@ -407,47 +488,49 @@ type ring struct {
|
||||||
// and can tolerate losing data when one of the servers dies.
|
// and can tolerate losing data when one of the servers dies.
|
||||||
// Otherwise you should use Redis Cluster.
|
// Otherwise you should use Redis Cluster.
|
||||||
type Ring struct {
|
type Ring struct {
|
||||||
*ring
|
|
||||||
cmdable
|
cmdable
|
||||||
hooks
|
hooksMixin
|
||||||
ctx context.Context
|
|
||||||
|
opt *RingOptions
|
||||||
|
sharding *ringSharding
|
||||||
|
cmdsInfoCache *cmdsInfoCache
|
||||||
|
heartbeatCancelFn context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRing(opt *RingOptions) *Ring {
|
func NewRing(opt *RingOptions) *Ring {
|
||||||
opt.init()
|
opt.init()
|
||||||
|
|
||||||
|
hbCtx, hbCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
ring := Ring{
|
ring := Ring{
|
||||||
ring: &ring{
|
opt: opt,
|
||||||
opt: opt,
|
sharding: newRingSharding(opt),
|
||||||
shards: newRingShards(opt),
|
heartbeatCancelFn: hbCancel,
|
||||||
},
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ring.cmdsInfoCache = newCmdsInfoCache(ring.cmdsInfo)
|
ring.cmdsInfoCache = newCmdsInfoCache(ring.cmdsInfo)
|
||||||
ring.cmdable = ring.Process
|
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
|
return &ring
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ring) Context() context.Context {
|
func (c *Ring) SetAddrs(addrs map[string]string) {
|
||||||
return c.ctx
|
c.sharding.SetAddrs(addrs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ring) WithContext(ctx context.Context) *Ring {
|
// Do create a Cmd from the args and processes the cmd.
|
||||||
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.
|
|
||||||
func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd {
|
func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||||
cmd := NewCmd(ctx, args...)
|
cmd := NewCmd(ctx, args...)
|
||||||
_ = c.Process(ctx, cmd)
|
_ = c.Process(ctx, cmd)
|
||||||
|
|
@ -455,7 +538,9 @@ func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ring) Process(ctx context.Context, cmd Cmder) error {
|
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.
|
// Options returns read-only Options that were used to create the client.
|
||||||
|
|
@ -469,7 +554,7 @@ func (c *Ring) retryBackoff(attempt int) time.Duration {
|
||||||
|
|
||||||
// PoolStats returns accumulated connection pool stats.
|
// PoolStats returns accumulated connection pool stats.
|
||||||
func (c *Ring) PoolStats() *PoolStats {
|
func (c *Ring) PoolStats() *PoolStats {
|
||||||
shards := c.shards.List()
|
shards := c.sharding.List()
|
||||||
var acc PoolStats
|
var acc PoolStats
|
||||||
for _, shard := range shards {
|
for _, shard := range shards {
|
||||||
s := shard.Client.connPool.Stats()
|
s := shard.Client.connPool.Stats()
|
||||||
|
|
@ -484,7 +569,7 @@ func (c *Ring) PoolStats() *PoolStats {
|
||||||
|
|
||||||
// Len returns the current number of shards in the ring.
|
// Len returns the current number of shards in the ring.
|
||||||
func (c *Ring) Len() int {
|
func (c *Ring) Len() int {
|
||||||
return c.shards.Len()
|
return c.sharding.Len()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subscribe subscribes the client to the specified channels.
|
// Subscribe subscribes the client to the specified channels.
|
||||||
|
|
@ -493,7 +578,7 @@ func (c *Ring) Subscribe(ctx context.Context, channels ...string) *PubSub {
|
||||||
panic("at least one channel is required")
|
panic("at least one channel is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
shard, err := c.shards.GetByKey(channels[0])
|
shard, err := c.sharding.GetByKey(channels[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: return PubSub with sticky error
|
// TODO: return PubSub with sticky error
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -507,7 +592,7 @@ func (c *Ring) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||||
panic("at least one channel is required")
|
panic("at least one channel is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
shard, err := c.shards.GetByKey(channels[0])
|
shard, err := c.sharding.GetByKey(channels[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: return PubSub with sticky error
|
// TODO: return PubSub with sticky error
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
@ -515,13 +600,30 @@ func (c *Ring) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
||||||
return shard.Client.PSubscribe(ctx, channels...)
|
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.
|
// ForEachShard concurrently calls the fn on each live shard in the ring.
|
||||||
// It returns the first error if any.
|
// It returns the first error if any.
|
||||||
func (c *Ring) ForEachShard(
|
func (c *Ring) ForEachShard(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
fn func(ctx context.Context, client *Client) error,
|
fn func(ctx context.Context, client *Client) error,
|
||||||
) error {
|
) error {
|
||||||
shards := c.shards.List()
|
shards := c.sharding.List()
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
for _, shard := range shards {
|
for _, shard := range shards {
|
||||||
|
|
@ -552,7 +654,7 @@ func (c *Ring) ForEachShard(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) {
|
func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) {
|
||||||
shards := c.shards.List()
|
shards := c.sharding.List()
|
||||||
var firstErr error
|
var firstErr error
|
||||||
for _, shard := range shards {
|
for _, shard := range shards {
|
||||||
cmdsInfo, err := shard.Client.Command(ctx).Result()
|
cmdsInfo, err := shard.Client.Command(ctx).Result()
|
||||||
|
|
@ -585,10 +687,10 @@ func (c *Ring) cmdShard(ctx context.Context, cmd Cmder) (*ringShard, error) {
|
||||||
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||||
pos := cmdFirstKeyPos(cmd, cmdInfo)
|
pos := cmdFirstKeyPos(cmd, cmdInfo)
|
||||||
if pos == 0 {
|
if pos == 0 {
|
||||||
return c.shards.Random()
|
return c.sharding.Random()
|
||||||
}
|
}
|
||||||
firstKey := cmd.stringArg(pos)
|
firstKey := cmd.stringArg(pos)
|
||||||
return c.shards.GetByKey(firstKey)
|
return c.sharding.GetByKey(firstKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ring) process(ctx context.Context, cmd Cmder) error {
|
func (c *Ring) process(ctx context.Context, cmd Cmder) error {
|
||||||
|
|
@ -619,47 +721,42 @@ func (c *Ring) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder
|
||||||
|
|
||||||
func (c *Ring) Pipeline() Pipeliner {
|
func (c *Ring) Pipeline() Pipeliner {
|
||||||
pipe := Pipeline{
|
pipe := Pipeline{
|
||||||
ctx: c.ctx,
|
exec: pipelineExecer(c.processPipelineHook),
|
||||||
exec: c.processPipeline,
|
|
||||||
}
|
}
|
||||||
pipe.init()
|
pipe.init()
|
||||||
return &pipe
|
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) {
|
func (c *Ring) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
||||||
return c.TxPipeline().Pipelined(ctx, fn)
|
return c.TxPipeline().Pipelined(ctx, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Ring) TxPipeline() Pipeliner {
|
func (c *Ring) TxPipeline() Pipeliner {
|
||||||
pipe := Pipeline{
|
pipe := Pipeline{
|
||||||
ctx: c.ctx,
|
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||||
exec: c.processTxPipeline,
|
cmds = wrapMultiExec(ctx, cmds)
|
||||||
|
return c.processTxPipelineHook(ctx, cmds)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
pipe.init()
|
pipe.init()
|
||||||
return &pipe
|
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(
|
func (c *Ring) generalProcessPipeline(
|
||||||
ctx context.Context, cmds []Cmder, tx bool,
|
ctx context.Context, cmds []Cmder, tx bool,
|
||||||
) error {
|
) error {
|
||||||
|
if tx {
|
||||||
|
// Trim multi .. exec.
|
||||||
|
cmds = cmds[1 : len(cmds)-1]
|
||||||
|
}
|
||||||
|
|
||||||
cmdsMap := make(map[string][]Cmder)
|
cmdsMap := make(map[string][]Cmder)
|
||||||
|
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
||||||
hash := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo))
|
hash := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo))
|
||||||
if hash != "" {
|
if hash != "" {
|
||||||
hash = c.shards.Hash(hash)
|
hash = c.sharding.Hash(hash)
|
||||||
}
|
}
|
||||||
cmdsMap[hash] = append(cmdsMap[hash], cmd)
|
cmdsMap[hash] = append(cmdsMap[hash], cmd)
|
||||||
}
|
}
|
||||||
|
|
@ -670,7 +767,19 @@ func (c *Ring) generalProcessPipeline(
|
||||||
go func(hash string, cmds []Cmder) {
|
go func(hash string, cmds []Cmder) {
|
||||||
defer wg.Done()
|
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)
|
}(hash, cmds)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -678,31 +787,16 @@ func (c *Ring) generalProcessPipeline(
|
||||||
return cmdsFirstErr(cmds)
|
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 {
|
func (c *Ring) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return fmt.Errorf("redis: Watch requires at least one key")
|
return fmt.Errorf("redis: Watch requires at least one key")
|
||||||
}
|
}
|
||||||
|
|
||||||
var shards []*ringShard
|
var shards []*ringShard
|
||||||
|
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
if key != "" {
|
if key != "" {
|
||||||
shard, err := c.shards.GetByKey(hashtag.Key(key))
|
shard, err := c.sharding.GetByKey(hashtag.Key(key))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -732,5 +826,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
|
// It is rare to Close a Ring, as the Ring is meant to be long-lived
|
||||||
// and shared between many goroutines.
|
// and shared between many goroutines.
|
||||||
func (c *Ring) Close() error {
|
func (c *Ring) Close() error {
|
||||||
return c.shards.Close()
|
c.heartbeatCancelFn()
|
||||||
|
|
||||||
|
return c.sharding.Close()
|
||||||
}
|
}
|
||||||
|
|
@ -5,12 +5,13 @@ import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Scripter interface {
|
type Scripter interface {
|
||||||
Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd
|
Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd
|
||||||
EvalSha(ctx context.Context, sha1 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
|
ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd
|
||||||
ScriptLoad(ctx context.Context, script string) *StringCmd
|
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...)
|
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 {
|
func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
||||||
return c.EvalSha(ctx, s.hash, keys, args...)
|
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
|
// Run optimistically uses EVALSHA to run the script. If script does not exist
|
||||||
// it is retried using EVAL.
|
// it is retried using EVAL.
|
||||||
func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
||||||
r := s.EvalSha(ctx, c, keys, args...)
|
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 s.Eval(ctx, c, keys, args...)
|
||||||
}
|
}
|
||||||
return r
|
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
|
||||||
|
}
|
||||||
|
|
@ -9,9 +9,9 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
"github.com/redis/go-redis/v9/internal"
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
"github.com/redis/go-redis/v9/internal/pool"
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
"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.
|
// A seed list of host:port addresses of sentinel nodes.
|
||||||
SentinelAddrs []string
|
SentinelAddrs []string
|
||||||
|
|
||||||
|
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
|
||||||
|
ClientName string
|
||||||
|
|
||||||
// If specified with SentinelPassword, enables ACL-based authentication (via
|
// If specified with SentinelPassword, enables ACL-based authentication (via
|
||||||
// AUTH <user> <pass>).
|
// AUTH <user> <pass>).
|
||||||
SentinelUsername string
|
SentinelUsername string
|
||||||
|
|
@ -32,25 +35,26 @@ type FailoverOptions struct {
|
||||||
// authentication.
|
// authentication.
|
||||||
SentinelPassword string
|
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.
|
// This option only works with NewFailoverClusterClient.
|
||||||
RouteByLatency bool
|
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.
|
// This option only works with NewFailoverClusterClient.
|
||||||
RouteRandomly bool
|
RouteRandomly bool
|
||||||
|
|
||||||
// Route all commands to slave read-only nodes.
|
// Route all commands to replica read-only nodes.
|
||||||
SlaveOnly bool
|
ReplicaOnly bool
|
||||||
|
|
||||||
// Use slaves disconnected with master when cannot get connected slaves
|
// Use replicas disconnected with master when cannot get connected replicas
|
||||||
// Now, this option only works in RandomSlaveAddr function.
|
// Now, this option only works in RandomReplicaAddr function.
|
||||||
UseDisconnectedSlaves bool
|
UseDisconnectedReplicas bool
|
||||||
|
|
||||||
// Following options are copied from Options struct.
|
// Following options are copied from Options struct.
|
||||||
|
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
OnConnect func(ctx context.Context, cn *Conn) error
|
||||||
|
|
||||||
|
Protocol int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
DB int
|
DB int
|
||||||
|
|
@ -59,31 +63,33 @@ type FailoverOptions struct {
|
||||||
MinRetryBackoff time.Duration
|
MinRetryBackoff time.Duration
|
||||||
MaxRetryBackoff time.Duration
|
MaxRetryBackoff time.Duration
|
||||||
|
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
WriteTimeout 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
|
PoolSize int
|
||||||
MinIdleConns int
|
PoolTimeout time.Duration
|
||||||
MaxConnAge time.Duration
|
MinIdleConns int
|
||||||
PoolTimeout time.Duration
|
MaxIdleConns int
|
||||||
IdleTimeout time.Duration
|
ConnMaxIdleTime time.Duration
|
||||||
IdleCheckFrequency time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opt *FailoverOptions) clientOptions() *Options {
|
func (opt *FailoverOptions) clientOptions() *Options {
|
||||||
return &Options{
|
return &Options{
|
||||||
Addr: "FailoverClient",
|
Addr: "FailoverClient",
|
||||||
|
ClientName: opt.ClientName,
|
||||||
|
|
||||||
Dialer: opt.Dialer,
|
Dialer: opt.Dialer,
|
||||||
OnConnect: opt.OnConnect,
|
OnConnect: opt.OnConnect,
|
||||||
|
|
||||||
DB: opt.DB,
|
DB: opt.DB,
|
||||||
|
Protocol: opt.Protocol,
|
||||||
Username: opt.Username,
|
Username: opt.Username,
|
||||||
Password: opt.Password,
|
Password: opt.Password,
|
||||||
|
|
||||||
|
|
@ -91,17 +97,18 @@ func (opt *FailoverOptions) clientOptions() *Options {
|
||||||
MinRetryBackoff: opt.MinRetryBackoff,
|
MinRetryBackoff: opt.MinRetryBackoff,
|
||||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
MaxRetryBackoff: opt.MaxRetryBackoff,
|
||||||
|
|
||||||
DialTimeout: opt.DialTimeout,
|
DialTimeout: opt.DialTimeout,
|
||||||
ReadTimeout: opt.ReadTimeout,
|
ReadTimeout: opt.ReadTimeout,
|
||||||
WriteTimeout: opt.WriteTimeout,
|
WriteTimeout: opt.WriteTimeout,
|
||||||
|
ContextTimeoutEnabled: opt.ContextTimeoutEnabled,
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
PoolFIFO: opt.PoolFIFO,
|
||||||
PoolSize: opt.PoolSize,
|
PoolSize: opt.PoolSize,
|
||||||
PoolTimeout: opt.PoolTimeout,
|
PoolTimeout: opt.PoolTimeout,
|
||||||
IdleTimeout: opt.IdleTimeout,
|
MinIdleConns: opt.MinIdleConns,
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
MaxIdleConns: opt.MaxIdleConns,
|
||||||
MinIdleConns: opt.MinIdleConns,
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||||
MaxConnAge: opt.MaxConnAge,
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
TLSConfig: opt.TLSConfig,
|
||||||
}
|
}
|
||||||
|
|
@ -109,7 +116,8 @@ func (opt *FailoverOptions) clientOptions() *Options {
|
||||||
|
|
||||||
func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
||||||
return &Options{
|
return &Options{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
|
ClientName: opt.ClientName,
|
||||||
|
|
||||||
Dialer: opt.Dialer,
|
Dialer: opt.Dialer,
|
||||||
OnConnect: opt.OnConnect,
|
OnConnect: opt.OnConnect,
|
||||||
|
|
@ -126,13 +134,13 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
||||||
ReadTimeout: opt.ReadTimeout,
|
ReadTimeout: opt.ReadTimeout,
|
||||||
WriteTimeout: opt.WriteTimeout,
|
WriteTimeout: opt.WriteTimeout,
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
PoolFIFO: opt.PoolFIFO,
|
||||||
PoolSize: opt.PoolSize,
|
PoolSize: opt.PoolSize,
|
||||||
PoolTimeout: opt.PoolTimeout,
|
PoolTimeout: opt.PoolTimeout,
|
||||||
IdleTimeout: opt.IdleTimeout,
|
MinIdleConns: opt.MinIdleConns,
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
MaxIdleConns: opt.MaxIdleConns,
|
||||||
MinIdleConns: opt.MinIdleConns,
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||||
MaxConnAge: opt.MaxConnAge,
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
TLSConfig: opt.TLSConfig,
|
||||||
}
|
}
|
||||||
|
|
@ -140,9 +148,12 @@ func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
||||||
|
|
||||||
func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
|
func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
|
||||||
return &ClusterOptions{
|
return &ClusterOptions{
|
||||||
|
ClientName: opt.ClientName,
|
||||||
|
|
||||||
Dialer: opt.Dialer,
|
Dialer: opt.Dialer,
|
||||||
OnConnect: opt.OnConnect,
|
OnConnect: opt.OnConnect,
|
||||||
|
|
||||||
|
Protocol: opt.Protocol,
|
||||||
Username: opt.Username,
|
Username: opt.Username,
|
||||||
Password: opt.Password,
|
Password: opt.Password,
|
||||||
|
|
||||||
|
|
@ -158,13 +169,13 @@ func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
|
||||||
ReadTimeout: opt.ReadTimeout,
|
ReadTimeout: opt.ReadTimeout,
|
||||||
WriteTimeout: opt.WriteTimeout,
|
WriteTimeout: opt.WriteTimeout,
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
PoolFIFO: opt.PoolFIFO,
|
||||||
PoolSize: opt.PoolSize,
|
PoolSize: opt.PoolSize,
|
||||||
PoolTimeout: opt.PoolTimeout,
|
PoolTimeout: opt.PoolTimeout,
|
||||||
IdleTimeout: opt.IdleTimeout,
|
MinIdleConns: opt.MinIdleConns,
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
MaxIdleConns: opt.MaxIdleConns,
|
||||||
MinIdleConns: opt.MinIdleConns,
|
ConnMaxIdleTime: opt.ConnMaxIdleTime,
|
||||||
MaxConnAge: opt.MaxConnAge,
|
ConnMaxLifetime: opt.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
TLSConfig: opt.TLSConfig,
|
||||||
}
|
}
|
||||||
|
|
@ -194,10 +205,21 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
|
||||||
}
|
}
|
||||||
|
|
||||||
opt := failoverOpt.clientOptions()
|
opt := failoverOpt.clientOptions()
|
||||||
opt.Dialer = masterSlaveDialer(failover)
|
opt.Dialer = masterReplicaDialer(failover)
|
||||||
opt.init()
|
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.mu.Lock()
|
||||||
failover.onFailover = func(ctx context.Context, addr string) {
|
failover.onFailover = func(ctx context.Context, addr string) {
|
||||||
|
|
@ -207,25 +229,18 @@ func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
|
||||||
}
|
}
|
||||||
failover.mu.Unlock()
|
failover.mu.Unlock()
|
||||||
|
|
||||||
c := Client{
|
return rdb
|
||||||
baseClient: newBaseClient(opt, connPool),
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
|
||||||
c.cmdable = c.Process
|
|
||||||
c.onClose = failover.Close
|
|
||||||
|
|
||||||
return &c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func masterSlaveDialer(
|
func masterReplicaDialer(
|
||||||
failover *sentinelFailover,
|
failover *sentinelFailover,
|
||||||
) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
return func(ctx context.Context, network, _ string) (net.Conn, error) {
|
return func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||||
var addr string
|
var addr string
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if failover.opt.SlaveOnly {
|
if failover.opt.ReplicaOnly {
|
||||||
addr, err = failover.RandomSlaveAddr(ctx)
|
addr, err = failover.RandomReplicaAddr(ctx)
|
||||||
} else {
|
} else {
|
||||||
addr, err = failover.MasterAddr(ctx)
|
addr, err = failover.MasterAddr(ctx)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
@ -255,37 +270,30 @@ func masterSlaveDialer(
|
||||||
// SentinelClient is a client for a Redis Sentinel.
|
// SentinelClient is a client for a Redis Sentinel.
|
||||||
type SentinelClient struct {
|
type SentinelClient struct {
|
||||||
*baseClient
|
*baseClient
|
||||||
hooks
|
hooksMixin
|
||||||
ctx context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSentinelClient(opt *Options) *SentinelClient {
|
func NewSentinelClient(opt *Options) *SentinelClient {
|
||||||
opt.init()
|
opt.init()
|
||||||
c := &SentinelClient{
|
c := &SentinelClient{
|
||||||
baseClient: &baseClient{
|
baseClient: &baseClient{
|
||||||
opt: opt,
|
opt: opt,
|
||||||
connPool: newConnPool(opt),
|
|
||||||
},
|
},
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.initHooks(hooks{
|
||||||
|
dial: c.baseClient.dial,
|
||||||
|
process: c.baseClient.process,
|
||||||
|
})
|
||||||
|
c.connPool = newConnPool(opt, c.dialHook)
|
||||||
|
|
||||||
return c
|
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 {
|
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 {
|
func (c *SentinelClient) pubSub() *PubSub {
|
||||||
|
|
@ -335,8 +343,8 @@ func (c *SentinelClient) GetMasterAddrByName(ctx context.Context, name string) *
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SentinelClient) Sentinels(ctx context.Context, name string) *SliceCmd {
|
func (c *SentinelClient) Sentinels(ctx context.Context, name string) *MapStringStringSliceCmd {
|
||||||
cmd := NewSliceCmd(ctx, "sentinel", "sentinels", name)
|
cmd := NewMapStringStringSliceCmd(ctx, "sentinel", "sentinels", name)
|
||||||
_ = c.Process(ctx, cmd)
|
_ = c.Process(ctx, cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
@ -351,7 +359,7 @@ func (c *SentinelClient) Failover(ctx context.Context, name string) *StatusCmd {
|
||||||
|
|
||||||
// Reset resets all the masters with matching name. The pattern argument is a
|
// 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
|
// 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.
|
// already discovered and associated with the master.
|
||||||
func (c *SentinelClient) Reset(ctx context.Context, pattern string) *IntCmd {
|
func (c *SentinelClient) Reset(ctx context.Context, pattern string) *IntCmd {
|
||||||
cmd := NewIntCmd(ctx, "sentinel", "reset", pattern)
|
cmd := NewIntCmd(ctx, "sentinel", "reset", pattern)
|
||||||
|
|
@ -368,8 +376,8 @@ func (c *SentinelClient) FlushConfig(ctx context.Context) *StatusCmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Master shows the state and info of the specified master.
|
// Master shows the state and info of the specified master.
|
||||||
func (c *SentinelClient) Master(ctx context.Context, name string) *StringStringMapCmd {
|
func (c *SentinelClient) Master(ctx context.Context, name string) *MapStringStringCmd {
|
||||||
cmd := NewStringStringMapCmd(ctx, "sentinel", "master", name)
|
cmd := NewMapStringStringCmd(ctx, "sentinel", "master", name)
|
||||||
_ = c.Process(ctx, cmd)
|
_ = c.Process(ctx, cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
@ -381,9 +389,9 @@ func (c *SentinelClient) Masters(ctx context.Context) *SliceCmd {
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slaves shows a list of slaves for the specified master and their state.
|
// Replicas shows a list of replicas for the specified master and their state.
|
||||||
func (c *SentinelClient) Slaves(ctx context.Context, name string) *SliceCmd {
|
func (c *SentinelClient) Replicas(ctx context.Context, name string) *MapStringStringSliceCmd {
|
||||||
cmd := NewSliceCmd(ctx, "sentinel", "slaves", name)
|
cmd := NewMapStringStringSliceCmd(ctx, "sentinel", "replicas", name)
|
||||||
_ = c.Process(ctx, cmd)
|
_ = c.Process(ctx, cmd)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
@ -460,18 +468,18 @@ func (c *sentinelFailover) closeSentinel() error {
|
||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *sentinelFailover) RandomSlaveAddr(ctx context.Context) (string, error) {
|
func (c *sentinelFailover) RandomReplicaAddr(ctx context.Context) (string, error) {
|
||||||
if c.opt == nil {
|
if c.opt == nil {
|
||||||
return "", errors.New("opt is nil")
|
return "", errors.New("opt is nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
addresses, err := c.slaveAddrs(ctx, false)
|
addresses, err := c.replicaAddrs(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(addresses) == 0 && c.opt.UseDisconnectedSlaves {
|
if len(addresses) == 0 && c.opt.UseDisconnectedReplicas {
|
||||||
addresses, err = c.slaveAddrs(ctx, true)
|
addresses, err = c.replicaAddrs(ctx, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
@ -489,8 +497,15 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
|
|
||||||
if sentinel != nil {
|
if sentinel != nil {
|
||||||
addr := c.getMasterAddr(ctx, sentinel)
|
addr, err := c.getMasterAddr(ctx, sentinel)
|
||||||
if addr != "" {
|
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
|
return addr, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -499,11 +514,18 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if c.sentinel != nil {
|
if c.sentinel != nil {
|
||||||
addr := c.getMasterAddr(ctx, c.sentinel)
|
addr, err := c.getMasterAddr(ctx, c.sentinel)
|
||||||
if addr != "" {
|
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
|
return addr, nil
|
||||||
}
|
}
|
||||||
_ = c.closeSentinel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, sentinelAddr := range c.sentinelAddrs {
|
for i, sentinelAddr := range c.sentinelAddrs {
|
||||||
|
|
@ -511,9 +533,12 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||||
|
|
||||||
masterAddr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
|
masterAddr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
|
||||||
if err != nil {
|
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",
|
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName master=%q failed: %s",
|
||||||
c.opt.MasterName, err)
|
c.opt.MasterName, err)
|
||||||
_ = sentinel.Close()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -528,14 +553,21 @@ func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
||||||
return "", errors.New("redis: all sentinels specified in configuration are unreachable")
|
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()
|
c.mu.RLock()
|
||||||
sentinel := c.sentinel
|
sentinel := c.sentinel
|
||||||
c.mu.RUnlock()
|
c.mu.RUnlock()
|
||||||
|
|
||||||
if sentinel != nil {
|
if sentinel != nil {
|
||||||
addrs := c.getSlaveAddrs(ctx, sentinel)
|
addrs, err := c.getReplicaAddrs(ctx, sentinel)
|
||||||
if len(addrs) > 0 {
|
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
|
return addrs, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -544,11 +576,21 @@ func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool)
|
||||||
defer c.mu.Unlock()
|
defer c.mu.Unlock()
|
||||||
|
|
||||||
if c.sentinel != nil {
|
if c.sentinel != nil {
|
||||||
addrs := c.getSlaveAddrs(ctx, c.sentinel)
|
addrs, err := c.getReplicaAddrs(ctx, c.sentinel)
|
||||||
if len(addrs) > 0 {
|
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
|
return addrs, nil
|
||||||
|
} else {
|
||||||
|
// No error and no replicas.
|
||||||
|
_ = c.closeSentinel()
|
||||||
}
|
}
|
||||||
_ = c.closeSentinel()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var sentinelReachable bool
|
var sentinelReachable bool
|
||||||
|
|
@ -556,15 +598,18 @@ func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool)
|
||||||
for i, sentinelAddr := range c.sentinelAddrs {
|
for i, sentinelAddr := range c.sentinelAddrs {
|
||||||
sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))
|
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 {
|
if err != nil {
|
||||||
internal.Logger.Printf(ctx, "sentinel: Slaves master=%q failed: %s",
|
|
||||||
c.opt.MasterName, err)
|
|
||||||
_ = sentinel.Close()
|
_ = 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
|
continue
|
||||||
}
|
}
|
||||||
sentinelReachable = true
|
sentinelReachable = true
|
||||||
addrs := parseSlaveAddrs(slaves, useDisconnected)
|
addrs := parseReplicaAddrs(replicas, useDisconnected)
|
||||||
if len(addrs) == 0 {
|
if len(addrs) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -581,60 +626,42 @@ func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool)
|
||||||
return []string{}, errors.New("redis: all sentinels specified in configuration are unreachable")
|
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()
|
addr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName name=%q failed: %s",
|
return "", err
|
||||||
c.opt.MasterName, err)
|
|
||||||
return ""
|
|
||||||
}
|
}
|
||||||
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 {
|
func (c *sentinelFailover) getReplicaAddrs(ctx context.Context, sentinel *SentinelClient) ([]string, error) {
|
||||||
addrs, err := sentinel.Slaves(ctx, c.opt.MasterName).Result()
|
addrs, err := sentinel.Replicas(ctx, c.opt.MasterName).Result()
|
||||||
if err != nil {
|
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)
|
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))
|
nodes := make([]string, 0, len(addrs))
|
||||||
for _, node := range addrs {
|
for _, node := range addrs {
|
||||||
ip := ""
|
|
||||||
port := ""
|
|
||||||
flags := []string{}
|
|
||||||
lastkey := ""
|
|
||||||
isDown := false
|
isDown := false
|
||||||
|
if flags, ok := node["flags"]; ok {
|
||||||
for _, key := range node.([]interface{}) {
|
for _, flag := range strings.Split(flags, ",") {
|
||||||
switch lastkey {
|
switch flag {
|
||||||
case "ip":
|
case "s_down", "o_down":
|
||||||
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 {
|
|
||||||
isDown = true
|
isDown = true
|
||||||
|
case "disconnected":
|
||||||
|
if !keepDisconnected {
|
||||||
|
isDown = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !isDown && node["ip"] != "" && node["port"] != "" {
|
||||||
if !isDown {
|
nodes = append(nodes, net.JoinHostPort(node["ip"], node["port"]))
|
||||||
nodes = append(nodes, net.JoinHostPort(ip, port))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -672,7 +699,7 @@ func (c *sentinelFailover) setSentinel(ctx context.Context, sentinel *SentinelCl
|
||||||
c.sentinel = sentinel
|
c.sentinel = sentinel
|
||||||
c.discoverSentinels(ctx)
|
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)
|
go c.listen(c.pubsub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -683,16 +710,13 @@ func (c *sentinelFailover) discoverSentinels(ctx context.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, sentinel := range sentinels {
|
for _, sentinel := range sentinels {
|
||||||
vals := sentinel.([]interface{})
|
ip, ok := sentinel["ip"]
|
||||||
var ip, port string
|
if !ok {
|
||||||
for i := 0; i < len(vals); i += 2 {
|
continue
|
||||||
key := vals[i].(string)
|
}
|
||||||
switch key {
|
port, ok := sentinel["port"]
|
||||||
case "ip":
|
if !ok {
|
||||||
ip = vals[i+1].(string)
|
continue
|
||||||
case "port":
|
|
||||||
port = vals[i+1].(string)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if ip != "" && port != "" {
|
if ip != "" && port != "" {
|
||||||
sentinelAddr := net.JoinHostPort(ip, port)
|
sentinelAddr := net.JoinHostPort(ip, port)
|
||||||
|
|
@ -742,7 +766,7 @@ func contains(slice []string, str string) bool {
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
// NewFailoverClusterClient returns a client that supports routing read-only commands
|
// NewFailoverClusterClient returns a client that supports routing read-only commands
|
||||||
// to a slave node.
|
// to a replica node.
|
||||||
func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
|
func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
|
||||||
sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
|
sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
|
||||||
copy(sentinelAddrs, failoverOpt.SentinelAddrs)
|
copy(sentinelAddrs, failoverOpt.SentinelAddrs)
|
||||||
|
|
@ -763,14 +787,14 @@ func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
|
||||||
Addr: masterAddr,
|
Addr: masterAddr,
|
||||||
}}
|
}}
|
||||||
|
|
||||||
slaveAddrs, err := failover.slaveAddrs(ctx, false)
|
replicaAddrs, err := failover.replicaAddrs(ctx, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, slaveAddr := range slaveAddrs {
|
for _, replicaAddr := range replicaAddrs {
|
||||||
nodes = append(nodes, ClusterNode{
|
nodes = append(nodes, ClusterNode{
|
||||||
Addr: slaveAddr,
|
Addr: replicaAddr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
56
vendor/github.com/go-redis/redis/v8/tx.go → vendor/github.com/redis/go-redis/v9/tx.go
generated
vendored
56
vendor/github.com/go-redis/redis/v8/tx.go → vendor/github.com/redis/go-redis/v9/tx.go
generated
vendored
|
|
@ -3,8 +3,8 @@ package redis
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
"github.com/redis/go-redis/v9/internal/pool"
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
"github.com/redis/go-redis/v9/internal/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TxFailedErr transaction redis failed.
|
// TxFailedErr transaction redis failed.
|
||||||
|
|
@ -19,18 +19,16 @@ type Tx struct {
|
||||||
baseClient
|
baseClient
|
||||||
cmdable
|
cmdable
|
||||||
statefulCmdable
|
statefulCmdable
|
||||||
hooks
|
hooksMixin
|
||||||
ctx context.Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) newTx(ctx context.Context) *Tx {
|
func (c *Client) newTx() *Tx {
|
||||||
tx := Tx{
|
tx := Tx{
|
||||||
baseClient: baseClient{
|
baseClient: baseClient{
|
||||||
opt: c.opt,
|
opt: c.opt,
|
||||||
connPool: pool.NewStickyConnPool(c.connPool),
|
connPool: pool.NewStickyConnPool(c.connPool),
|
||||||
},
|
},
|
||||||
hooks: c.hooks.clone(),
|
hooksMixin: c.hooksMixin.clone(),
|
||||||
ctx: ctx,
|
|
||||||
}
|
}
|
||||||
tx.init()
|
tx.init()
|
||||||
return &tx
|
return &tx
|
||||||
|
|
@ -39,25 +37,19 @@ func (c *Client) newTx(ctx context.Context) *Tx {
|
||||||
func (c *Tx) init() {
|
func (c *Tx) init() {
|
||||||
c.cmdable = c.Process
|
c.cmdable = c.Process
|
||||||
c.statefulCmdable = c.Process
|
c.statefulCmdable = c.Process
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Tx) Context() context.Context {
|
c.initHooks(hooks{
|
||||||
return c.ctx
|
dial: c.baseClient.dial,
|
||||||
}
|
process: c.baseClient.process,
|
||||||
|
pipeline: c.baseClient.processPipeline,
|
||||||
func (c *Tx) WithContext(ctx context.Context) *Tx {
|
txPipeline: c.baseClient.processTxPipeline,
|
||||||
if ctx == nil {
|
})
|
||||||
panic("nil context")
|
|
||||||
}
|
|
||||||
clone := *c
|
|
||||||
clone.init()
|
|
||||||
clone.hooks.lock()
|
|
||||||
clone.ctx = ctx
|
|
||||||
return &clone
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Tx) Process(ctx context.Context, cmd Cmder) error {
|
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
|
// 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.
|
// The transaction is automatically closed when fn exits.
|
||||||
func (c *Client) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
|
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)
|
defer tx.Close(ctx)
|
||||||
if len(keys) > 0 {
|
if len(keys) > 0 {
|
||||||
if err := tx.Watch(ctx, keys...).Err(); err != nil {
|
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.
|
// Pipeline creates a pipeline. Usually it is more convenient to use Pipelined.
|
||||||
func (c *Tx) Pipeline() Pipeliner {
|
func (c *Tx) Pipeline() Pipeliner {
|
||||||
pipe := Pipeline{
|
pipe := Pipeline{
|
||||||
ctx: c.ctx,
|
|
||||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
exec: func(ctx context.Context, cmds []Cmder) error {
|
||||||
return c.hooks.processPipeline(ctx, cmds, c.baseClient.processPipeline)
|
return c.processPipelineHook(ctx, cmds)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
pipe.init()
|
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.
|
// TxPipeline creates a pipeline. Usually it is more convenient to use TxPipelined.
|
||||||
func (c *Tx) TxPipeline() Pipeliner {
|
func (c *Tx) TxPipeline() Pipeliner {
|
||||||
pipe := Pipeline{
|
pipe := Pipeline{
|
||||||
ctx: c.ctx,
|
|
||||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
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()
|
pipe.init()
|
||||||
return &pipe
|
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
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,9 @@ type UniversalOptions struct {
|
||||||
// of cluster/sentinel nodes.
|
// of cluster/sentinel nodes.
|
||||||
Addrs []string
|
Addrs []string
|
||||||
|
|
||||||
|
// ClientName will execute the `CLIENT SETNAME ClientName` command for each conn.
|
||||||
|
ClientName string
|
||||||
|
|
||||||
// Database to be selected after connecting to the server.
|
// Database to be selected after connecting to the server.
|
||||||
// Only single-node and failover clients.
|
// Only single-node and failover clients.
|
||||||
DB int
|
DB int
|
||||||
|
|
@ -23,6 +26,7 @@ type UniversalOptions struct {
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
OnConnect func(ctx context.Context, cn *Conn) error
|
||||||
|
|
||||||
|
Protocol int
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
SentinelUsername string
|
SentinelUsername string
|
||||||
|
|
@ -32,19 +36,20 @@ type UniversalOptions struct {
|
||||||
MinRetryBackoff time.Duration
|
MinRetryBackoff time.Duration
|
||||||
MaxRetryBackoff time.Duration
|
MaxRetryBackoff time.Duration
|
||||||
|
|
||||||
DialTimeout time.Duration
|
DialTimeout time.Duration
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
WriteTimeout time.Duration
|
WriteTimeout time.Duration
|
||||||
|
ContextTimeoutEnabled bool
|
||||||
|
|
||||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
||||||
PoolFIFO bool
|
PoolFIFO bool
|
||||||
|
|
||||||
PoolSize int
|
PoolSize int
|
||||||
MinIdleConns int
|
PoolTimeout time.Duration
|
||||||
MaxConnAge time.Duration
|
MinIdleConns int
|
||||||
PoolTimeout time.Duration
|
MaxIdleConns int
|
||||||
IdleTimeout time.Duration
|
ConnMaxIdleTime time.Duration
|
||||||
IdleCheckFrequency time.Duration
|
ConnMaxLifetime time.Duration
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
TLSConfig *tls.Config
|
||||||
|
|
||||||
|
|
@ -68,10 +73,12 @@ func (o *UniversalOptions) Cluster() *ClusterOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &ClusterOptions{
|
return &ClusterOptions{
|
||||||
Addrs: o.Addrs,
|
Addrs: o.Addrs,
|
||||||
Dialer: o.Dialer,
|
ClientName: o.ClientName,
|
||||||
OnConnect: o.OnConnect,
|
Dialer: o.Dialer,
|
||||||
|
OnConnect: o.OnConnect,
|
||||||
|
|
||||||
|
Protocol: o.Protocol,
|
||||||
Username: o.Username,
|
Username: o.Username,
|
||||||
Password: o.Password,
|
Password: o.Password,
|
||||||
|
|
||||||
|
|
@ -84,16 +91,19 @@ func (o *UniversalOptions) Cluster() *ClusterOptions {
|
||||||
MinRetryBackoff: o.MinRetryBackoff,
|
MinRetryBackoff: o.MinRetryBackoff,
|
||||||
MaxRetryBackoff: o.MaxRetryBackoff,
|
MaxRetryBackoff: o.MaxRetryBackoff,
|
||||||
|
|
||||||
DialTimeout: o.DialTimeout,
|
DialTimeout: o.DialTimeout,
|
||||||
ReadTimeout: o.ReadTimeout,
|
ReadTimeout: o.ReadTimeout,
|
||||||
WriteTimeout: o.WriteTimeout,
|
WriteTimeout: o.WriteTimeout,
|
||||||
PoolFIFO: o.PoolFIFO,
|
ContextTimeoutEnabled: o.ContextTimeoutEnabled,
|
||||||
PoolSize: o.PoolSize,
|
|
||||||
MinIdleConns: o.MinIdleConns,
|
PoolFIFO: o.PoolFIFO,
|
||||||
MaxConnAge: o.MaxConnAge,
|
|
||||||
PoolTimeout: o.PoolTimeout,
|
PoolSize: o.PoolSize,
|
||||||
IdleTimeout: o.IdleTimeout,
|
PoolTimeout: o.PoolTimeout,
|
||||||
IdleCheckFrequency: o.IdleCheckFrequency,
|
MinIdleConns: o.MinIdleConns,
|
||||||
|
MaxIdleConns: o.MaxIdleConns,
|
||||||
|
ConnMaxIdleTime: o.ConnMaxIdleTime,
|
||||||
|
ConnMaxLifetime: o.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: o.TLSConfig,
|
TLSConfig: o.TLSConfig,
|
||||||
}
|
}
|
||||||
|
|
@ -108,11 +118,13 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
|
||||||
return &FailoverOptions{
|
return &FailoverOptions{
|
||||||
SentinelAddrs: o.Addrs,
|
SentinelAddrs: o.Addrs,
|
||||||
MasterName: o.MasterName,
|
MasterName: o.MasterName,
|
||||||
|
ClientName: o.ClientName,
|
||||||
|
|
||||||
Dialer: o.Dialer,
|
Dialer: o.Dialer,
|
||||||
OnConnect: o.OnConnect,
|
OnConnect: o.OnConnect,
|
||||||
|
|
||||||
DB: o.DB,
|
DB: o.DB,
|
||||||
|
Protocol: o.Protocol,
|
||||||
Username: o.Username,
|
Username: o.Username,
|
||||||
Password: o.Password,
|
Password: o.Password,
|
||||||
SentinelUsername: o.SentinelUsername,
|
SentinelUsername: o.SentinelUsername,
|
||||||
|
|
@ -122,17 +134,18 @@ func (o *UniversalOptions) Failover() *FailoverOptions {
|
||||||
MinRetryBackoff: o.MinRetryBackoff,
|
MinRetryBackoff: o.MinRetryBackoff,
|
||||||
MaxRetryBackoff: o.MaxRetryBackoff,
|
MaxRetryBackoff: o.MaxRetryBackoff,
|
||||||
|
|
||||||
DialTimeout: o.DialTimeout,
|
DialTimeout: o.DialTimeout,
|
||||||
ReadTimeout: o.ReadTimeout,
|
ReadTimeout: o.ReadTimeout,
|
||||||
WriteTimeout: o.WriteTimeout,
|
WriteTimeout: o.WriteTimeout,
|
||||||
|
ContextTimeoutEnabled: o.ContextTimeoutEnabled,
|
||||||
|
|
||||||
PoolFIFO: o.PoolFIFO,
|
PoolFIFO: o.PoolFIFO,
|
||||||
PoolSize: o.PoolSize,
|
PoolSize: o.PoolSize,
|
||||||
MinIdleConns: o.MinIdleConns,
|
PoolTimeout: o.PoolTimeout,
|
||||||
MaxConnAge: o.MaxConnAge,
|
MinIdleConns: o.MinIdleConns,
|
||||||
PoolTimeout: o.PoolTimeout,
|
MaxIdleConns: o.MaxIdleConns,
|
||||||
IdleTimeout: o.IdleTimeout,
|
ConnMaxIdleTime: o.ConnMaxIdleTime,
|
||||||
IdleCheckFrequency: o.IdleCheckFrequency,
|
ConnMaxLifetime: o.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: o.TLSConfig,
|
TLSConfig: o.TLSConfig,
|
||||||
}
|
}
|
||||||
|
|
@ -146,11 +159,13 @@ func (o *UniversalOptions) Simple() *Options {
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Options{
|
return &Options{
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
Dialer: o.Dialer,
|
ClientName: o.ClientName,
|
||||||
OnConnect: o.OnConnect,
|
Dialer: o.Dialer,
|
||||||
|
OnConnect: o.OnConnect,
|
||||||
|
|
||||||
DB: o.DB,
|
DB: o.DB,
|
||||||
|
Protocol: o.Protocol,
|
||||||
Username: o.Username,
|
Username: o.Username,
|
||||||
Password: o.Password,
|
Password: o.Password,
|
||||||
|
|
||||||
|
|
@ -158,17 +173,18 @@ func (o *UniversalOptions) Simple() *Options {
|
||||||
MinRetryBackoff: o.MinRetryBackoff,
|
MinRetryBackoff: o.MinRetryBackoff,
|
||||||
MaxRetryBackoff: o.MaxRetryBackoff,
|
MaxRetryBackoff: o.MaxRetryBackoff,
|
||||||
|
|
||||||
DialTimeout: o.DialTimeout,
|
DialTimeout: o.DialTimeout,
|
||||||
ReadTimeout: o.ReadTimeout,
|
ReadTimeout: o.ReadTimeout,
|
||||||
WriteTimeout: o.WriteTimeout,
|
WriteTimeout: o.WriteTimeout,
|
||||||
|
ContextTimeoutEnabled: o.ContextTimeoutEnabled,
|
||||||
|
|
||||||
PoolFIFO: o.PoolFIFO,
|
PoolFIFO: o.PoolFIFO,
|
||||||
PoolSize: o.PoolSize,
|
PoolSize: o.PoolSize,
|
||||||
MinIdleConns: o.MinIdleConns,
|
PoolTimeout: o.PoolTimeout,
|
||||||
MaxConnAge: o.MaxConnAge,
|
MinIdleConns: o.MinIdleConns,
|
||||||
PoolTimeout: o.PoolTimeout,
|
MaxIdleConns: o.MaxIdleConns,
|
||||||
IdleTimeout: o.IdleTimeout,
|
ConnMaxIdleTime: o.ConnMaxIdleTime,
|
||||||
IdleCheckFrequency: o.IdleCheckFrequency,
|
ConnMaxLifetime: o.ConnMaxLifetime,
|
||||||
|
|
||||||
TLSConfig: o.TLSConfig,
|
TLSConfig: o.TLSConfig,
|
||||||
}
|
}
|
||||||
|
|
@ -182,13 +198,13 @@ func (o *UniversalOptions) Simple() *Options {
|
||||||
// clients in different environments.
|
// clients in different environments.
|
||||||
type UniversalClient interface {
|
type UniversalClient interface {
|
||||||
Cmdable
|
Cmdable
|
||||||
Context() context.Context
|
|
||||||
AddHook(Hook)
|
AddHook(Hook)
|
||||||
Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error
|
Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error
|
||||||
Do(ctx context.Context, args ...interface{}) *Cmd
|
Do(ctx context.Context, args ...interface{}) *Cmd
|
||||||
Process(ctx context.Context, cmd Cmder) error
|
Process(ctx context.Context, cmd Cmder) error
|
||||||
Subscribe(ctx context.Context, channels ...string) *PubSub
|
Subscribe(ctx context.Context, channels ...string) *PubSub
|
||||||
PSubscribe(ctx context.Context, channels ...string) *PubSub
|
PSubscribe(ctx context.Context, channels ...string) *PubSub
|
||||||
|
SSubscribe(ctx context.Context, channels ...string) *PubSub
|
||||||
Close() error
|
Close() error
|
||||||
PoolStats() *PoolStats
|
PoolStats() *PoolStats
|
||||||
}
|
}
|
||||||
|
|
@ -2,5 +2,5 @@ package redis
|
||||||
|
|
||||||
// Version is the current release version.
|
// Version is the current release version.
|
||||||
func Version() string {
|
func Version() string {
|
||||||
return "8.11.5"
|
return "9.1.0"
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +130,8 @@ github.com/eggsampler/acme/v3
|
||||||
# github.com/felixge/httpsnoop v1.0.3
|
# github.com/felixge/httpsnoop v1.0.3
|
||||||
## explicit; go 1.13
|
## explicit; go 1.13
|
||||||
github.com/felixge/httpsnoop
|
github.com/felixge/httpsnoop
|
||||||
|
# github.com/fsnotify/fsnotify v1.4.9
|
||||||
|
## explicit; go 1.13
|
||||||
# github.com/go-logr/logr v1.2.4
|
# github.com/go-logr/logr v1.2.4
|
||||||
## explicit; go 1.16
|
## explicit; go 1.16
|
||||||
github.com/go-logr/logr
|
github.com/go-logr/logr
|
||||||
|
|
@ -144,16 +146,6 @@ github.com/go-playground/locales/currency
|
||||||
# github.com/go-playground/universal-translator v0.18.1
|
# github.com/go-playground/universal-translator v0.18.1
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
github.com/go-playground/universal-translator
|
github.com/go-playground/universal-translator
|
||||||
# github.com/go-redis/redis/v8 v8.11.5
|
|
||||||
## explicit; go 1.17
|
|
||||||
github.com/go-redis/redis/v8
|
|
||||||
github.com/go-redis/redis/v8/internal
|
|
||||||
github.com/go-redis/redis/v8/internal/hashtag
|
|
||||||
github.com/go-redis/redis/v8/internal/hscan
|
|
||||||
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/go-redis/redis/v8/internal/util
|
|
||||||
# github.com/go-sql-driver/mysql v1.5.0
|
# github.com/go-sql-driver/mysql v1.5.0
|
||||||
## explicit; go 1.10
|
## explicit; go 1.10
|
||||||
github.com/go-sql-driver/mysql
|
github.com/go-sql-driver/mysql
|
||||||
|
|
@ -241,6 +233,16 @@ github.com/prometheus/common/model
|
||||||
github.com/prometheus/procfs
|
github.com/prometheus/procfs
|
||||||
github.com/prometheus/procfs/internal/fs
|
github.com/prometheus/procfs/internal/fs
|
||||||
github.com/prometheus/procfs/internal/util
|
github.com/prometheus/procfs/internal/util
|
||||||
|
# github.com/redis/go-redis/v9 v9.1.0
|
||||||
|
## explicit; go 1.18
|
||||||
|
github.com/redis/go-redis/v9
|
||||||
|
github.com/redis/go-redis/v9/internal
|
||||||
|
github.com/redis/go-redis/v9/internal/hashtag
|
||||||
|
github.com/redis/go-redis/v9/internal/hscan
|
||||||
|
github.com/redis/go-redis/v9/internal/pool
|
||||||
|
github.com/redis/go-redis/v9/internal/proto
|
||||||
|
github.com/redis/go-redis/v9/internal/rand
|
||||||
|
github.com/redis/go-redis/v9/internal/util
|
||||||
# github.com/rogpeppe/go-internal v1.9.0
|
# github.com/rogpeppe/go-internal v1.9.0
|
||||||
## explicit; go 1.17
|
## explicit; go 1.17
|
||||||
# github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399
|
# github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue