From 366fd2d6cdabfe75c4a64a05acaee7dece7bdad1 Mon Sep 17 00:00:00 2001 From: Cyril Levis Date: Mon, 11 Dec 2023 16:07:05 +0100 Subject: [PATCH] feat: add redis sentinel support --- README.md | 12 ++--- backends/config/config_test.go | 2 + backends/redis.go | 55 +++++++++++++++++------ backends/redis_test.go | 2 +- config/backends.go | 11 ++++- config/config.go | 2 + config/config_test.go | 9 +++- config/configtest/sample_full_config.yaml | 6 ++- endpoints/put_test.go | 2 +- go.mod | 6 ++- go.sum | 15 +++++-- 11 files changed, 91 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 6ac453ea..68f2fc95 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ This section does not describe permanent API contracts; it just describes limita ## Backend Configuration -Prebid Cache requires a backend data store which enforces TTL expiration. The following storage options are supported: Aerospike, Cassandra, Memcache, and Redis. You're welcomed to contribute a new backend adapter if needed. +Prebid Cache requires a backend data store which enforces TTL expiration. The following storage options are supported: Aerospike, Cassandra, Memcache, and Redis/Redis sentinel. You're welcomed to contribute a new backend adapter if needed. There is also an option (enabled by default) for a basic in-memory data store intended only for development. This backend does not support TTL expiration and is not built for production use. @@ -245,8 +245,10 @@ Prebid Cache makes use of a Cassandra client that supports latest 3 major releas Prebid Cache makes use of a Redis Go client compatible with Redis 6. Full documentation of the Redis Go client Prebid Cache uses can be found [here](https://github.com/go-redis/redis). | Configuration field | Type | Description | | --- | --- | --- | -| host | string | Redis server URI | -| port | integer | Redis server port | +| host | string | Redis server URI (redis standalone mode) | +| port | integer | Redis server port (redis standalone mode) | +| hosts |string array | Redis server sentinel URI (redis sentinel mode)| +| mastername | string | Redis master sentinel name (redis sentinel mode)| | password | string | Redis password | | db | integer | Database to be selected after connecting to the server | | expiration | integer | Availability in the Redis system in Minutes | @@ -281,7 +283,7 @@ backend: hosts: "127.0.0.1" keyspace: "prebid" memcache: - hosts: ["10.0.0.1:11211","127.0.0.1"] + hosts: ["10.0.0.1:11211", "127.0.0.1"] redis: host: "127.0.0.1" port: 6379 @@ -381,4 +383,4 @@ docker run -p 8000:8000 -t prebid-cache ### Profiling -[pprof stats](http://artem.krylysov.com/blog/2017/03/13/profiling-and-optimizing-go-web-applications/) can be accessed from a running app on the admin port `localhost:2525`. \ No newline at end of file +[pprof stats](http://artem.krylysov.com/blog/2017/03/13/profiling-and-optimizing-go-web-applications/) can be accessed from a running app on the admin port `localhost:2525`. diff --git a/backends/config/config_test.go b/backends/config/config_test.go index ea72d46a..48a2de75 100644 --- a/backends/config/config_test.go +++ b/backends/config/config_test.go @@ -149,6 +149,8 @@ func TestNewBaseBackend(t *testing.T) { desc: "Redis", inConfig: config.Backend{Type: config.BackendRedis}, expectedLogEntries: []logEntry{ + {msg: "Error creating Redis backend: At least one Host[s] is required.", lvl: logrus.FatalLevel}, + {msg: "Creating Redis backend", lvl: logrus.InfoLevel}, {msg: "Error creating Redis backend: ", lvl: logrus.FatalLevel}, }, }, diff --git a/backends/redis.go b/backends/redis.go index d7128555..569d03a7 100644 --- a/backends/redis.go +++ b/backends/redis.go @@ -6,9 +6,9 @@ import ( "strconv" "time" - "github.com/go-redis/redis/v8" "github.com/prebid/prebid-cache/config" "github.com/prebid/prebid-cache/utils" + "github.com/redis/go-redis/v9" log "github.com/sirupsen/logrus" ) @@ -47,26 +47,49 @@ type RedisBackend struct { // NewRedisBackend initializes the redis client and pings to make sure connection was successful func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { - constr := cfg.Host + ":" + strconv.Itoa(cfg.Port) - options := &redis.Options{ - Addr: constr, - Password: cfg.Password, - DB: cfg.Db, + if len(cfg.Hosts) < 1 || cfg.Host == "" { + log.Fatalf("Error creating Redis backend: At least one Host[s] is required.") } - if cfg.TLS.Enabled { - options = &redis.Options{ + var redisClient RedisDBClient + + sentinel := false + + if cfg.MasterName != "" { + sentinel = true + // Mode failover (sentinel) + log.Info("Creating Redis sentinel backend") + options := &redis.FailoverOptions{ + MasterName: cfg.MasterName, + SentinelAddrs: cfg.Hosts, + DB: cfg.Db, + Password: cfg.Password, + SentinelPassword: cfg.Password, + } + + if cfg.TLS.Enabled { + options.TLSConfig = &tls.Config{InsecureSkipVerify: cfg.TLS.InsecureSkipVerify} + } + + redisClient = RedisDBClient{client: redis.NewFailoverClient(options)} + } else { + // Mode single + log.Info("Creating Redis backend") + constr := cfg.Host + ":" + strconv.Itoa(cfg.Port) + options := &redis.Options{ Addr: constr, Password: cfg.Password, DB: cfg.Db, - TLSConfig: &tls.Config{ - InsecureSkipVerify: cfg.TLS.InsecureSkipVerify, - }, } - } - redisClient := RedisDBClient{client: redis.NewClient(options)} + if cfg.TLS.Enabled { + options.TLSConfig = &tls.Config{InsecureSkipVerify: cfg.TLS.InsecureSkipVerify} + } + + redisClient = RedisDBClient{client: redis.NewClient(options)} + + } _, err := redisClient.client.Ping(ctx).Result() @@ -75,7 +98,11 @@ func NewRedisBackend(cfg config.Redis, ctx context.Context) *RedisBackend { panic("RedisBackend failure. This shouldn't happen.") } - log.Infof("Connected to Redis at %s:%d", cfg.Host, cfg.Port) + if sentinel { + log.Infof("Connected to Redis: %v", cfg.Hosts) + } else { + log.Infof("Connected to Redis: %s:%v", cfg.Host, cfg.Port) + } return &RedisBackend{ cfg: cfg, diff --git a/backends/redis_test.go b/backends/redis_test.go index 7238ae5c..2e8d3c94 100644 --- a/backends/redis_test.go +++ b/backends/redis_test.go @@ -5,8 +5,8 @@ import ( "errors" "testing" - "github.com/go-redis/redis/v8" "github.com/prebid/prebid-cache/utils" + "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" ) diff --git a/config/backends.go b/config/backends.go index 2f5c8694..1783b9c7 100644 --- a/config/backends.go +++ b/config/backends.go @@ -157,6 +157,8 @@ type Redis struct { Db int `mapstructure:"db"` ExpirationMinutes int `mapstructure:"expiration"` TLS RedisTLS `mapstructure:"tls"` + Hosts []string `mapstructure:"hosts"` + MasterName string `mapstructure:"mastername"` } type RedisTLS struct { @@ -165,8 +167,13 @@ type RedisTLS struct { } func (cfg *Redis) validateAndLog() error { - log.Infof("config.backend.redis.host: %s", cfg.Host) - log.Infof("config.backend.redis.port: %d", cfg.Port) + if cfg.Host != "" && len(cfg.Hosts) > 0 { + log.Infof("config.backend.redis.hosts: %s. Note that redis host will be ignore if 'hosts' is define", cfg.Hosts) + log.Infof("config.backend.redis.mastername: %s.", cfg.MasterName) + } else { + log.Infof("config.backend.redis.host: %s", cfg.Host) + log.Infof("config.backend.redis.port: %d", cfg.Port) + } log.Infof("config.backend.redis.db: %d", cfg.Db) if cfg.ExpirationMinutes > 0 { log.Infof("config.backend.redis.expiration: %d. Note that this configuration option is being deprecated in favor of config.request_limits.max_ttl_seconds", cfg.ExpirationMinutes) diff --git a/config/config.go b/config/config.go index 59e02080..ffa3f256 100644 --- a/config/config.go +++ b/config/config.go @@ -63,6 +63,8 @@ func setConfigDefaults(v *viper.Viper) { v.SetDefault("backend.cassandra.default_ttl_seconds", utils.CASSANDRA_DEFAULT_TTL_SECONDS) v.SetDefault("backend.memcache.hosts", []string{}) v.SetDefault("backend.redis.host", "") + v.SetDefault("backend.redis.hosts", []string{}) + v.SetDefault("backend.redis.mastername", "") v.SetDefault("backend.redis.port", 0) v.SetDefault("backend.redis.password", "") v.SetDefault("backend.redis.db", 0) diff --git a/config/config_test.go b/config/config_test.go index 8ab6da94..b3878f38 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1202,6 +1202,7 @@ func getExpectedDefaultConfig() Configuration { DefaultTTL: utils.CASSANDRA_DEFAULT_TTL_SECONDS, }, Redis: Redis{ + Hosts: []string{}, ExpirationMinutes: utils.REDIS_DEFAULT_EXPIRATION_MINUTES, }, Ignite: Ignite{ @@ -1267,8 +1268,12 @@ func getExpectedFullConfigForTestFile() Configuration { Hosts: []string{"10.0.0.1:11211", "127.0.0.1"}, }, Redis: Redis{ - Host: "127.0.0.1", - Port: 6379, + Host: "127.0.0.1", + Port: 6379, + Hosts: []string{ + "10.0.0.1:26379", "127.0.0.1", + }, + MasterName: "mymaster", Password: "redis-password", Db: 1, ExpirationMinutes: 1, diff --git a/config/configtest/sample_full_config.yaml b/config/configtest/sample_full_config.yaml index 3c40fbb7..5d814cae 100644 --- a/config/configtest/sample_full_config.yaml +++ b/config/configtest/sample_full_config.yaml @@ -27,8 +27,12 @@ backend: keyspace: "prebid" default_ttl_seconds: 60 memcache: - hosts: ["10.0.0.1:11211","127.0.0.1"] + hosts: ["10.0.0.1:11211", "127.0.0.1"] redis: + hosts: + - "10.0.0.1:26379" + - "127.0.0.1" + mastername: "mymaster" host: "127.0.0.1" port: 6379 password: "redis-password" diff --git a/endpoints/put_test.go b/endpoints/put_test.go index abc143f0..2c9b265c 100644 --- a/endpoints/put_test.go +++ b/endpoints/put_test.go @@ -15,7 +15,6 @@ import ( "testing" "time" - "github.com/go-redis/redis/v8" "github.com/gofrs/uuid" "github.com/julienschmidt/httprouter" "github.com/prebid/prebid-cache/backends" @@ -26,6 +25,7 @@ import ( "github.com/prebid/prebid-cache/metrics" "github.com/prebid/prebid-cache/metrics/metricstest" "github.com/prebid/prebid-cache/utils" + "github.com/redis/go-redis/v9" "github.com/sirupsen/logrus" testLogrus "github.com/sirupsen/logrus/hooks/test" "github.com/spf13/viper" diff --git a/go.mod b/go.mod index c3e28311..d5f0cc61 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/aerospike/aerospike-client-go/v6 v6.7.0 github.com/didip/tollbooth/v6 v6.1.2 - github.com/go-redis/redis/v8 v8.11.5 github.com/gocql/gocql v1.0.0 github.com/gofrs/uuid v4.2.0+incompatible github.com/golang/snappy v0.0.4 @@ -14,6 +13,7 @@ require ( github.com/prometheus/client_golang v1.12.2 github.com/prometheus/client_model v0.2.0 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 + github.com/redis/go-redis/v9 v9.3.0 github.com/rs/cors v1.8.2 github.com/sirupsen/logrus v1.6.0 github.com/spf13/viper v1.11.0 @@ -24,7 +24,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-hostpool v0.1.0 // indirect - github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/fsnotify/fsnotify v1.5.1 // indirect @@ -37,6 +37,8 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect + github.com/onsi/ginkgo v1.16.5 // indirect + github.com/onsi/gomega v1.18.1 // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pelletier/go-toml/v2 v2.0.0-beta.8 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/go.sum b/go.sum index f01caaca..0474f0b0 100644 --- a/go.sum +++ b/go.sum @@ -54,10 +54,13 @@ github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2io github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -93,8 +96,6 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-pkgz/expirable-cache v0.0.3 h1:rTh6qNPp78z0bQE6HDhXBHUwqnV9i09Vm6dksJLXQDc= github.com/go-pkgz/expirable-cache v0.0.3/go.mod h1:+IauqN00R2FqNRLCLA+X5YljQJrwB179PfiAoMPlTlQ= -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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gocql/gocql v1.0.0 h1:UnbTERpP72VZ/viKE1Q1gPtmLvyTZTvuAstvSRydw/c= @@ -162,6 +163,7 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -223,10 +225,14 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= +github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.0-beta.8 h1:dy81yyLYJDwMTifq24Oi/IslOslRrDSb3jwDggjz3Z0= @@ -263,6 +269,8 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.3.0 h1:RiVDjmig62jIWp7Kk4XVLs0hzV6pI3PyTnnL0cnn0u0= +github.com/redis/go-redis/v9 v9.3.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= @@ -466,6 +474,7 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f h1:v4INt8xihDGvnrfjMDVXGxw9wrfxYyCjk0KbXjhR55s= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=