From e1b63989b2361920a243c8c051044451c3fafab9 Mon Sep 17 00:00:00 2001 From: Conrad Hoffmann Date: Tue, 12 Nov 2024 17:18:17 +0100 Subject: [PATCH] redis: use UniveralClient, support sentinel mode This commit switches the redis client to using a UniversalClient [1], which can operate in standard, cluster, or failover (sentinel) mode. It also implements basic support for having (multiple) `redis+sentinel://` URLs as connection URL in the config (e.g. for `redis-host`). This is the Go equivalent of the proposed Python patch [2]. It supports the same examples given there. [1] https://pkg.go.dev/github.com/go-redis/redis/v8#UniversalClient [2] https://lists.sr.ht/~sircmpwn/sr.ht-dev/patches/55521 --- go.mod | 2 +- redis/middleware.go | 8 ++-- redis/url.go | 104 ++++++++++++++++++++++++++++++++++++++++++++ server/server.go | 6 +-- 4 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 redis/url.go diff --git a/go.mod b/go.mod index d8d198e1da106f03b14bfbb85d60fb560ca954fe..55a5c256a10e8233893ba45b93d702d82940685a 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module git.sr.ht/~sircmpwn/core-go -go 1.17 +go 1.18 require ( git.sr.ht/~sircmpwn/dowork v0.0.0-20221010085743-46c4299d76a1 diff --git a/redis/middleware.go b/redis/middleware.go index 769f3bdfd21bde949159d3b185e33ae0c8c964bf..bc9da61fdaac0aa09edf56c8c63024e521007632 100644 --- a/redis/middleware.go +++ b/redis/middleware.go @@ -13,7 +13,7 @@ type contextKey struct { name string } -func Middleware(client *redis.Client) func(http.Handler) http.Handler { +func Middleware(client redis.UniversalClient) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ctx := Context(r.Context(), client) @@ -23,12 +23,12 @@ func Middleware(client *redis.Client) func(http.Handler) http.Handler { } } -func Context(ctx context.Context, client *redis.Client) context.Context { +func Context(ctx context.Context, client redis.UniversalClient) context.Context { return context.WithValue(ctx, redisCtxKey, client) } -func ForContext(ctx context.Context) *redis.Client { - raw, ok := ctx.Value(redisCtxKey).(*redis.Client) +func ForContext(ctx context.Context) redis.UniversalClient { + raw, ok := ctx.Value(redisCtxKey).(redis.UniversalClient) if !ok { panic("Invalid redis context") } diff --git a/redis/url.go b/redis/url.go new file mode 100644 index 0000000000000000000000000000000000000000..e9f719863d3c0102811829d6d8f5e1290f7fbb81 --- /dev/null +++ b/redis/url.go @@ -0,0 +1,104 @@ +package redis + +import ( + "crypto/tls" + "fmt" + "net/url" + "slices" + "strconv" + "strings" + + "github.com/go-redis/redis/v8" +) + +func parseSentinelURLs(urls []*url.URL) (*redis.UniversalOptions, error) { + uopts := redis.UniversalOptions{} + + var schemes []string + var usernames []string + var passwords []string + + for _, u := range urls { + if !slices.Contains(schemes, u.Scheme) { + schemes = append(schemes, u.Scheme) + } + if !slices.Contains(usernames, u.User.Username()) { + usernames = append(usernames, u.User.Username()) + } + password, _ := u.User.Password() + if !slices.Contains(passwords, password) { + passwords = append(passwords, password) + } + if u.Scheme != "redis+sentinel" && u.Scheme != "rediss+sentinel" { + return nil, fmt.Errorf("invalid connection URL scheme: %s", u.Scheme) + } + uopts.Addrs = append(uopts.Addrs, u.Host) + } + + // For global options, force uniformity + if len(schemes) > 1 { + return nil, fmt.Errorf("connection URLs must have uniform scheme") + } + if len(usernames) > 1 { + return nil, fmt.Errorf("connection URLs must have uniform password") + } + if len(passwords) > 1 { + return nil, fmt.Errorf("connection URLs must have uniform password") + } + + u := urls[0] + path := strings.Split(u.Path[1:], "/") + uopts.MasterName = path[0] + if len(path) == 2 { + db, err := strconv.Atoi(path[1]) + if err != nil { + return nil, err + } + uopts.DB = db + } else if len(path) > 2 { + return nil, fmt.Errorf("invalid connection URL path: %s", u.Path) + } + uopts.Username = u.User.Username() + uopts.SentinelUsername = u.User.Username() + uopts.Password, _ = u.User.Password() + uopts.SentinelPassword, _ = u.User.Password() + if u.Scheme == "rediss+sentinel" { + uopts.TLSConfig = &tls.Config{ServerName: u.Hostname()} + } + return &uopts, nil +} + +func ParseURL(raw string) (*redis.UniversalOptions, error) { + // Support multiple URLs for sentinel connections + var schemes []string + var urls []*url.URL + + for _, r := range strings.Split(raw, ",") { + u, err := url.Parse(r) + if err != nil { + return nil, err + } + urls = append(urls, u) + if !slices.Contains(schemes, u.Scheme) { + schemes = append(schemes, u.Scheme) + } + } + + if len(urls) == 1 { + if urls[0].Scheme == "redis" || urls[0].Scheme == "rediss" { + opts, err := redis.ParseURL(raw) + if err != nil { + return nil, err + } + return &redis.UniversalOptions{ + Addrs: []string{opts.Addr}, + // TODO + }, nil + } + if urls[0].Scheme != "redis+sentinel" && urls[0].Scheme != "rediss+sentinel" { + return nil, fmt.Errorf("invalid connection URL scheme: %s", urls[0].Scheme) + } + // a single sentinel URL, fall through to parsing that + } + return parseSentinelURLs(urls) +} diff --git a/server/server.go b/server/server.go index 5c68c9886ecd466e90eef96a73a4c18e6f20b73d..92547f9b8109a756c0765f9bdbd42f2f35be5a5d 100644 --- a/server/server.go +++ b/server/server.go @@ -52,7 +52,7 @@ type Server struct { conf ini.File db *sql.DB - redis *goRedis.Client + redis goRedis.UniversalClient root chi.Router router chi.Router service string @@ -174,11 +174,11 @@ func (server *Server) WithDefaultMiddleware() *Server { if !ok { rcs = "redis://" } - ropts, err := goRedis.ParseURL(rcs) + ropts, err := redis.ParseURL(rcs) if err != nil { log.Fatalf("Invalid sr.ht::redis-host in config.ini: %v", err) } - rc := goRedis.NewClient(ropts) + rc := goRedis.NewUniversalClient(ropts) server.redis = rc apiconf := fmt.Sprintf("%s::api", server.service)