mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-16 04:29:15 +00:00
Add redis support for distributed caching (#83)
* Add redis support for distributed caching * Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi. * ... and another all nighter. * fixup! ... and another all nighter. * fixup! fixup! ... and another all nighter. * fixup! fixup! fixup! ... and another all nighter. * Resolve issue #85 by adding ability to set custom claims in JWT tokens * Remove redundant validation in auth middleware ( issue #89 ) * Add ability to set cookie prefix for session cookies ( #87 ) * fixup! Add ability to set cookie prefix for session cookies ( #87 ) * Add ability to set cookie max age - issue #91 * Potential fix for code scanning alert no. 10: Size computation for allocation may overflow Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fixup! Merge main into 0.8.0-redis: resolve conflicts --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
parent
5fcbd54955
commit
e64fc7f730
+759
@@ -0,0 +1,759 @@
|
||||
// Package miniredis is a pure Go Redis test server, for use in Go unittests.
|
||||
// There are no dependencies on system binaries, and every server you start
|
||||
// will be empty.
|
||||
//
|
||||
// import "github.com/alicebob/miniredis/v2"
|
||||
//
|
||||
// Start a server with `s := miniredis.RunT(t)`, it'll be shutdown via a t.Cleanup().
|
||||
// Or do everything manual: `s, err := miniredis.Run(); defer s.Close()`
|
||||
//
|
||||
// Point your Redis client to `s.Addr()` or `s.Host(), s.Port()`.
|
||||
//
|
||||
// Set keys directly via s.Set(...) and similar commands, or use a Redis client.
|
||||
//
|
||||
// For direct use you can select a Redis database with either `s.Select(12);
|
||||
// s.Get("foo")` or `s.DB(12).Get("foo")`.
|
||||
package miniredis
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/alicebob/miniredis/v2/proto"
|
||||
"github.com/alicebob/miniredis/v2/server"
|
||||
)
|
||||
|
||||
var DumpMaxLineLen = 60
|
||||
|
||||
type hashKey map[string]string
|
||||
type listKey []string
|
||||
type setKey map[string]struct{}
|
||||
|
||||
// RedisDB holds a single (numbered) Redis database.
|
||||
type RedisDB struct {
|
||||
master *Miniredis // pointer to the lock in Miniredis
|
||||
id int // db id
|
||||
keys map[string]string // Master map of keys with their type
|
||||
stringKeys map[string]string // GET/SET &c. keys
|
||||
hashKeys map[string]hashKey // MGET/MSET &c. keys
|
||||
listKeys map[string]listKey // LPUSH &c. keys
|
||||
setKeys map[string]setKey // SADD &c. keys
|
||||
hllKeys map[string]*hll // PFADD &c. keys
|
||||
sortedsetKeys map[string]sortedSet // ZADD &c. keys
|
||||
streamKeys map[string]*streamKey // XADD &c. keys
|
||||
ttl map[string]time.Duration // effective TTL values
|
||||
lru map[string]time.Time // last recently used ( read or written to )
|
||||
keyVersion map[string]uint // used to watch values
|
||||
}
|
||||
|
||||
// Miniredis is a Redis server implementation.
|
||||
type Miniredis struct {
|
||||
sync.Mutex
|
||||
srv *server.Server
|
||||
port int
|
||||
passwords map[string]string // username password
|
||||
dbs map[int]*RedisDB
|
||||
selectedDB int // DB id used in the direct Get(), Set() &c.
|
||||
scripts map[string]string // sha1 -> lua src
|
||||
signal *sync.Cond
|
||||
now time.Time // time.Now() if not set.
|
||||
subscribers map[*Subscriber]struct{}
|
||||
rand *rand.Rand
|
||||
Ctx context.Context
|
||||
CtxCancel context.CancelFunc
|
||||
}
|
||||
|
||||
type txCmd func(*server.Peer, *connCtx)
|
||||
|
||||
// database id + key combo
|
||||
type dbKey struct {
|
||||
db int
|
||||
key string
|
||||
}
|
||||
|
||||
// connCtx has all state for a single connection.
|
||||
// (this struct was named before context.Context existed)
|
||||
type connCtx struct {
|
||||
selectedDB int // selected DB
|
||||
authenticated bool // auth enabled and a valid AUTH seen
|
||||
transaction []txCmd // transaction callbacks. Or nil.
|
||||
dirtyTransaction bool // any error during QUEUEing
|
||||
watch map[dbKey]uint // WATCHed keys
|
||||
subscriber *Subscriber // client is in PUBSUB mode if not nil
|
||||
nested bool // this is called via Lua
|
||||
nestedSHA string // set to the SHA of the nesting function
|
||||
}
|
||||
|
||||
// NewMiniRedis makes a new, non-started, Miniredis object.
|
||||
func NewMiniRedis() *Miniredis {
|
||||
m := Miniredis{
|
||||
dbs: map[int]*RedisDB{},
|
||||
scripts: map[string]string{},
|
||||
subscribers: map[*Subscriber]struct{}{},
|
||||
}
|
||||
m.Ctx, m.CtxCancel = context.WithCancel(context.Background())
|
||||
m.signal = sync.NewCond(&m)
|
||||
return &m
|
||||
}
|
||||
|
||||
func newRedisDB(id int, m *Miniredis) RedisDB {
|
||||
return RedisDB{
|
||||
id: id,
|
||||
master: m,
|
||||
keys: map[string]string{},
|
||||
lru: map[string]time.Time{},
|
||||
stringKeys: map[string]string{},
|
||||
hashKeys: map[string]hashKey{},
|
||||
listKeys: map[string]listKey{},
|
||||
setKeys: map[string]setKey{},
|
||||
hllKeys: map[string]*hll{},
|
||||
sortedsetKeys: map[string]sortedSet{},
|
||||
streamKeys: map[string]*streamKey{},
|
||||
ttl: map[string]time.Duration{},
|
||||
keyVersion: map[string]uint{},
|
||||
}
|
||||
}
|
||||
|
||||
// Run creates and Start()s a Miniredis.
|
||||
func Run() (*Miniredis, error) {
|
||||
m := NewMiniRedis()
|
||||
return m, m.Start()
|
||||
}
|
||||
|
||||
// Run creates and Start()s a Miniredis, TLS version.
|
||||
func RunTLS(cfg *tls.Config) (*Miniredis, error) {
|
||||
m := NewMiniRedis()
|
||||
return m, m.StartTLS(cfg)
|
||||
}
|
||||
|
||||
// Tester is a minimal version of a testing.T
|
||||
type Tester interface {
|
||||
Fatalf(string, ...interface{})
|
||||
Cleanup(func())
|
||||
Logf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
// RunT start a new miniredis, pass it a testing.T. It also registers the cleanup after your test is done.
|
||||
func RunT(t Tester) *Miniredis {
|
||||
m := NewMiniRedis()
|
||||
if err := m.Start(); err != nil {
|
||||
t.Fatalf("could not start miniredis: %s", err)
|
||||
// not reached
|
||||
}
|
||||
t.Cleanup(m.Close)
|
||||
return m
|
||||
}
|
||||
|
||||
func runWithClient(t Tester) (*Miniredis, *proto.Client) {
|
||||
m := RunT(t)
|
||||
|
||||
c, err := proto.Dial(m.Addr())
|
||||
if err != nil {
|
||||
t.Fatalf("could not connect to miniredis: %s", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err = c.Close(); err != nil {
|
||||
t.Logf("error closing connection to miniredis: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
return m, c
|
||||
}
|
||||
|
||||
// Start starts a server. It listens on a random port on localhost. See also
|
||||
// Addr().
|
||||
func (m *Miniredis) Start() error {
|
||||
s, err := server.NewServer(fmt.Sprintf("127.0.0.1:%d", m.port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.start(s)
|
||||
}
|
||||
|
||||
// Start starts a server, TLS version.
|
||||
func (m *Miniredis) StartTLS(cfg *tls.Config) error {
|
||||
s, err := server.NewServerTLS(fmt.Sprintf("127.0.0.1:%d", m.port), cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.start(s)
|
||||
}
|
||||
|
||||
// StartAddr runs miniredis with a given addr. Examples: "127.0.0.1:6379",
|
||||
// ":6379", or "127.0.0.1:0"
|
||||
func (m *Miniredis) StartAddr(addr string) error {
|
||||
s, err := server.NewServer(addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.start(s)
|
||||
}
|
||||
|
||||
// StartAddrTLS runs miniredis with a given addr, TLS version.
|
||||
func (m *Miniredis) StartAddrTLS(addr string, cfg *tls.Config) error {
|
||||
s, err := server.NewServerTLS(addr, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return m.start(s)
|
||||
}
|
||||
|
||||
func (m *Miniredis) start(s *server.Server) error {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
m.srv = s
|
||||
m.port = s.Addr().Port
|
||||
|
||||
commandsConnection(m)
|
||||
commandsGeneric(m)
|
||||
commandsServer(m)
|
||||
commandsString(m)
|
||||
commandsHash(m)
|
||||
commandsList(m)
|
||||
commandsPubsub(m)
|
||||
commandsSet(m)
|
||||
commandsSortedSet(m)
|
||||
commandsStream(m)
|
||||
commandsTransaction(m)
|
||||
commandsScripting(m)
|
||||
commandsGeo(m)
|
||||
commandsCluster(m)
|
||||
commandsHll(m)
|
||||
commandsClient(m)
|
||||
commandsObject(m)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Restart restarts a Close()d server on the same port. Values will be
|
||||
// preserved.
|
||||
func (m *Miniredis) Restart() error {
|
||||
return m.Start()
|
||||
}
|
||||
|
||||
// Close shuts down a Miniredis.
|
||||
func (m *Miniredis) Close() {
|
||||
m.Lock()
|
||||
|
||||
if m.srv == nil {
|
||||
m.Unlock()
|
||||
return
|
||||
}
|
||||
srv := m.srv
|
||||
m.srv = nil
|
||||
m.CtxCancel()
|
||||
m.Unlock()
|
||||
|
||||
// the OnDisconnect callbacks can lock m, so run Close() outside the lock.
|
||||
srv.Close()
|
||||
|
||||
}
|
||||
|
||||
// RequireAuth makes every connection need to AUTH first. This is the old 'AUTH [password] command.
|
||||
// Remove it by setting an empty string.
|
||||
func (m *Miniredis) RequireAuth(pw string) {
|
||||
m.RequireUserAuth("default", pw)
|
||||
}
|
||||
|
||||
// Add a username/password, for use with 'AUTH [username] [password]'.
|
||||
// There are currently no access controls for commands implemented.
|
||||
// Disable access for the user with an empty password.
|
||||
func (m *Miniredis) RequireUserAuth(username, pw string) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if m.passwords == nil {
|
||||
m.passwords = map[string]string{}
|
||||
}
|
||||
if pw == "" {
|
||||
delete(m.passwords, username)
|
||||
return
|
||||
}
|
||||
m.passwords[username] = pw
|
||||
}
|
||||
|
||||
// DB returns a DB by ID.
|
||||
func (m *Miniredis) DB(i int) *RedisDB {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.db(i)
|
||||
}
|
||||
|
||||
// get DB. No locks!
|
||||
func (m *Miniredis) db(i int) *RedisDB {
|
||||
if db, ok := m.dbs[i]; ok {
|
||||
return db
|
||||
}
|
||||
db := newRedisDB(i, m) // main miniredis has our mutex.
|
||||
m.dbs[i] = &db
|
||||
return &db
|
||||
}
|
||||
|
||||
// SwapDB swaps DBs by IDs.
|
||||
func (m *Miniredis) SwapDB(i, j int) bool {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.swapDB(i, j)
|
||||
}
|
||||
|
||||
// swap DB. No locks!
|
||||
func (m *Miniredis) swapDB(i, j int) bool {
|
||||
db1 := m.db(i)
|
||||
db2 := m.db(j)
|
||||
|
||||
db1.id = j
|
||||
db2.id = i
|
||||
|
||||
m.dbs[i] = db2
|
||||
m.dbs[j] = db1
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Addr returns '127.0.0.1:12345'. Can be given to a Dial(). See also Host()
|
||||
// and Port(), which return the same things.
|
||||
func (m *Miniredis) Addr() string {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.srv.Addr().String()
|
||||
}
|
||||
|
||||
// Host returns the host part of Addr().
|
||||
func (m *Miniredis) Host() string {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.srv.Addr().IP.String()
|
||||
}
|
||||
|
||||
// Port returns the (random) port part of Addr().
|
||||
func (m *Miniredis) Port() string {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return strconv.Itoa(m.srv.Addr().Port)
|
||||
}
|
||||
|
||||
// CommandCount returns the number of processed commands.
|
||||
func (m *Miniredis) CommandCount() int {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return int(m.srv.TotalCommands())
|
||||
}
|
||||
|
||||
// CurrentConnectionCount returns the number of currently connected clients.
|
||||
func (m *Miniredis) CurrentConnectionCount() int {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return m.srv.ClientsLen()
|
||||
}
|
||||
|
||||
// TotalConnectionCount returns the number of client connections since server start.
|
||||
func (m *Miniredis) TotalConnectionCount() int {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
return int(m.srv.TotalConnections())
|
||||
}
|
||||
|
||||
// FastForward decreases all TTLs by the given duration. All TTLs <= 0 will be
|
||||
// expired.
|
||||
func (m *Miniredis) FastForward(duration time.Duration) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
for _, db := range m.dbs {
|
||||
db.fastForward(duration)
|
||||
}
|
||||
}
|
||||
|
||||
// Server returns the underlying server to allow custom commands to be implemented
|
||||
func (m *Miniredis) Server() *server.Server {
|
||||
return m.srv
|
||||
}
|
||||
|
||||
// Dump returns a text version of the selected DB, usable for debugging.
|
||||
//
|
||||
// Dump limits the maximum length of each key:value to "DumpMaxLineLen" characters.
|
||||
// To increase that, call something like:
|
||||
//
|
||||
// miniredis.DumpMaxLineLen = 1024
|
||||
// mr, _ = miniredis.Run()
|
||||
// mr.Dump()
|
||||
func (m *Miniredis) Dump() string {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
var (
|
||||
maxLen = DumpMaxLineLen
|
||||
indent = " "
|
||||
db = m.db(m.selectedDB)
|
||||
r = ""
|
||||
v = func(s string) string {
|
||||
suffix := ""
|
||||
if len(s) > maxLen {
|
||||
suffix = fmt.Sprintf("...(%d)", len(s))
|
||||
s = s[:maxLen-len(suffix)]
|
||||
}
|
||||
return fmt.Sprintf("%q%s", s, suffix)
|
||||
}
|
||||
)
|
||||
|
||||
for _, k := range db.allKeys() {
|
||||
r += fmt.Sprintf("- %s\n", k)
|
||||
t := db.t(k)
|
||||
switch t {
|
||||
case keyTypeString:
|
||||
r += fmt.Sprintf("%s%s\n", indent, v(db.stringKeys[k]))
|
||||
case keyTypeHash:
|
||||
for _, hk := range db.hashFields(k) {
|
||||
r += fmt.Sprintf("%s%s: %s\n", indent, hk, v(db.hashGet(k, hk)))
|
||||
}
|
||||
case keyTypeList:
|
||||
for _, lk := range db.listKeys[k] {
|
||||
r += fmt.Sprintf("%s%s\n", indent, v(lk))
|
||||
}
|
||||
case keyTypeSet:
|
||||
for _, mk := range db.setMembers(k) {
|
||||
r += fmt.Sprintf("%s%s\n", indent, v(mk))
|
||||
}
|
||||
case keyTypeSortedSet:
|
||||
for _, el := range db.ssetElements(k) {
|
||||
r += fmt.Sprintf("%s%f: %s\n", indent, el.score, v(el.member))
|
||||
}
|
||||
case keyTypeStream:
|
||||
for _, entry := range db.streamKeys[k].entries {
|
||||
r += fmt.Sprintf("%s%s\n", indent, entry.ID)
|
||||
ev := entry.Values
|
||||
for i := 0; i < len(ev)/2; i++ {
|
||||
r += fmt.Sprintf("%s%s%s: %s\n", indent, indent, v(ev[2*i]), v(ev[2*i+1]))
|
||||
}
|
||||
}
|
||||
case keyTypeHll:
|
||||
for _, entry := range db.hllKeys {
|
||||
r += fmt.Sprintf("%s%s\n", indent, v(string(entry.Bytes())))
|
||||
}
|
||||
default:
|
||||
r += fmt.Sprintf("%s(a %s, fixme!)\n", indent, t)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SetTime sets the time against which EXPIREAT values are compared, and the
|
||||
// time used in stream entry IDs. Will use time.Now() if this is not set.
|
||||
func (m *Miniredis) SetTime(t time.Time) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
m.now = t
|
||||
}
|
||||
|
||||
// make every command return this message. For example:
|
||||
//
|
||||
// LOADING Redis is loading the dataset in memory
|
||||
// MASTERDOWN Link with MASTER is down and replica-serve-stale-data is set to 'no'.
|
||||
//
|
||||
// Clear it with an empty string. Don't add newlines.
|
||||
func (m *Miniredis) SetError(msg string) {
|
||||
cb := server.Hook(nil)
|
||||
if msg != "" {
|
||||
cb = func(c *server.Peer, cmd string, args ...string) bool {
|
||||
c.WriteError(msg)
|
||||
return true
|
||||
}
|
||||
}
|
||||
m.srv.SetPreHook(cb)
|
||||
}
|
||||
|
||||
// isValidCMD returns true if command is valid and can be executed.
|
||||
func (m *Miniredis) isValidCMD(c *server.Peer, cmd string) bool {
|
||||
if !m.handleAuth(c) {
|
||||
return false
|
||||
}
|
||||
if m.checkPubsub(c, cmd) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// handleAuth returns false if connection has no access. It sends the reply.
|
||||
func (m *Miniredis) handleAuth(c *server.Peer) bool {
|
||||
if getCtx(c).nested {
|
||||
return true
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
if len(m.passwords) == 0 {
|
||||
return true
|
||||
}
|
||||
if !getCtx(c).authenticated {
|
||||
c.WriteError("NOAUTH Authentication required.")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// handlePubsub sends an error to the user if the connection is in PUBSUB mode.
|
||||
// It'll return true if it did.
|
||||
func (m *Miniredis) checkPubsub(c *server.Peer, cmd string) bool {
|
||||
if getCtx(c).nested {
|
||||
return false
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
ctx := getCtx(c)
|
||||
if ctx.subscriber == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
prefix := "ERR "
|
||||
if strings.ToLower(cmd) == "exec" {
|
||||
prefix = "EXECABORT Transaction discarded because of: "
|
||||
}
|
||||
c.WriteError(fmt.Sprintf(
|
||||
"%sCan't execute '%s': only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT are allowed in this context",
|
||||
prefix,
|
||||
strings.ToLower(cmd),
|
||||
))
|
||||
return true
|
||||
}
|
||||
|
||||
func getCtx(c *server.Peer) *connCtx {
|
||||
if c.Ctx == nil {
|
||||
c.Ctx = &connCtx{}
|
||||
}
|
||||
return c.Ctx.(*connCtx)
|
||||
}
|
||||
|
||||
func startTx(ctx *connCtx) {
|
||||
ctx.transaction = []txCmd{}
|
||||
ctx.dirtyTransaction = false
|
||||
}
|
||||
|
||||
func stopTx(ctx *connCtx) {
|
||||
ctx.transaction = nil
|
||||
unwatch(ctx)
|
||||
}
|
||||
|
||||
func inTx(ctx *connCtx) bool {
|
||||
return ctx.transaction != nil
|
||||
}
|
||||
|
||||
func addTxCmd(ctx *connCtx, cb txCmd) {
|
||||
ctx.transaction = append(ctx.transaction, cb)
|
||||
}
|
||||
|
||||
func watch(db *RedisDB, ctx *connCtx, key string) {
|
||||
if ctx.watch == nil {
|
||||
ctx.watch = map[dbKey]uint{}
|
||||
}
|
||||
ctx.watch[dbKey{db: db.id, key: key}] = db.keyVersion[key] // Can be 0.
|
||||
}
|
||||
|
||||
func unwatch(ctx *connCtx) {
|
||||
ctx.watch = nil
|
||||
}
|
||||
|
||||
// setDirty can be called even when not in an tx. Is an no-op then.
|
||||
func setDirty(c *server.Peer) {
|
||||
if c.Ctx == nil {
|
||||
// No transaction. Not relevant.
|
||||
return
|
||||
}
|
||||
getCtx(c).dirtyTransaction = true
|
||||
}
|
||||
|
||||
func (m *Miniredis) addSubscriber(s *Subscriber) {
|
||||
m.subscribers[s] = struct{}{}
|
||||
}
|
||||
|
||||
// closes and remove the subscriber.
|
||||
func (m *Miniredis) removeSubscriber(s *Subscriber) {
|
||||
_, ok := m.subscribers[s]
|
||||
delete(m.subscribers, s)
|
||||
if ok {
|
||||
s.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Miniredis) publish(c, msg string) int {
|
||||
n := 0
|
||||
for s := range m.subscribers {
|
||||
n += s.Publish(c, msg)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// enter 'subscribed state', or return the existing one.
|
||||
func (m *Miniredis) subscribedState(c *server.Peer) *Subscriber {
|
||||
ctx := getCtx(c)
|
||||
sub := ctx.subscriber
|
||||
if sub != nil {
|
||||
return sub
|
||||
}
|
||||
|
||||
sub = newSubscriber()
|
||||
m.addSubscriber(sub)
|
||||
|
||||
c.OnDisconnect(func() {
|
||||
m.Lock()
|
||||
m.removeSubscriber(sub)
|
||||
m.Unlock()
|
||||
})
|
||||
|
||||
ctx.subscriber = sub
|
||||
|
||||
go monitorPublish(c, sub.publish)
|
||||
go monitorPpublish(c, sub.ppublish)
|
||||
|
||||
return sub
|
||||
}
|
||||
|
||||
// whenever the p?sub count drops to 0 subscribed state should be stopped, and
|
||||
// all redis commands are allowed again.
|
||||
func endSubscriber(m *Miniredis, c *server.Peer) {
|
||||
ctx := getCtx(c)
|
||||
if sub := ctx.subscriber; sub != nil {
|
||||
m.removeSubscriber(sub) // will Close() the sub
|
||||
}
|
||||
ctx.subscriber = nil
|
||||
}
|
||||
|
||||
// Start a new pubsub subscriber. It can (un) subscribe to channels and
|
||||
// patterns, and has a channel to get published messages. Close it with
|
||||
// Close().
|
||||
// Does not close itself when there are no subscriptions left.
|
||||
func (m *Miniredis) NewSubscriber() *Subscriber {
|
||||
sub := newSubscriber()
|
||||
|
||||
m.Lock()
|
||||
m.addSubscriber(sub)
|
||||
m.Unlock()
|
||||
|
||||
return sub
|
||||
}
|
||||
|
||||
func (m *Miniredis) allSubscribers() []*Subscriber {
|
||||
var subs []*Subscriber
|
||||
for s := range m.subscribers {
|
||||
subs = append(subs, s)
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
||||
func (m *Miniredis) Seed(seed int) {
|
||||
m.Lock()
|
||||
defer m.Unlock()
|
||||
|
||||
// m.rand is not safe for concurrent use.
|
||||
m.rand = rand.New(rand.NewSource(int64(seed)))
|
||||
}
|
||||
|
||||
func (m *Miniredis) randIntn(n int) int {
|
||||
if m.rand == nil {
|
||||
return rand.Intn(n)
|
||||
}
|
||||
return m.rand.Intn(n)
|
||||
}
|
||||
|
||||
// shuffle shuffles a list of strings. Kinda.
|
||||
func (m *Miniredis) shuffle(l []string) {
|
||||
for range l {
|
||||
i := m.randIntn(len(l))
|
||||
j := m.randIntn(len(l))
|
||||
l[i], l[j] = l[j], l[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Miniredis) effectiveNow() time.Time {
|
||||
if !m.now.IsZero() {
|
||||
return m.now
|
||||
}
|
||||
return time.Now().UTC()
|
||||
}
|
||||
|
||||
// convert a unixtimestamp to a duration, to use an absolute time as TTL.
|
||||
// d can be either time.Second or time.Millisecond.
|
||||
func (m *Miniredis) at(i int, d time.Duration) time.Duration {
|
||||
var ts time.Time
|
||||
switch d {
|
||||
case time.Millisecond:
|
||||
ts = time.Unix(int64(i/1000), 1000000*int64(i%1000))
|
||||
case time.Second:
|
||||
ts = time.Unix(int64(i), 0)
|
||||
default:
|
||||
panic("invalid time unit (d). Fixme!")
|
||||
}
|
||||
now := m.effectiveNow()
|
||||
return ts.Sub(now)
|
||||
}
|
||||
|
||||
// copy does not mind if dst already exists.
|
||||
func (m *Miniredis) copy(
|
||||
srcDB *RedisDB, src string,
|
||||
destDB *RedisDB, dst string,
|
||||
) error {
|
||||
if !srcDB.exists(src) {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
|
||||
switch srcDB.t(src) {
|
||||
case keyTypeString:
|
||||
destDB.stringKeys[dst] = srcDB.stringKeys[src]
|
||||
case keyTypeHash:
|
||||
destDB.hashKeys[dst] = copyHashKey(srcDB.hashKeys[src])
|
||||
case keyTypeList:
|
||||
destDB.listKeys[dst] = copyListKey(srcDB.listKeys[src])
|
||||
case keyTypeSet:
|
||||
destDB.setKeys[dst] = copySetKey(srcDB.setKeys[src])
|
||||
case keyTypeSortedSet:
|
||||
destDB.sortedsetKeys[dst] = copySortedSet(srcDB.sortedsetKeys[src])
|
||||
case keyTypeStream:
|
||||
destDB.streamKeys[dst] = srcDB.streamKeys[src].copy()
|
||||
case keyTypeHll:
|
||||
destDB.hllKeys[dst] = srcDB.hllKeys[src].copy()
|
||||
default:
|
||||
panic("missing case")
|
||||
}
|
||||
destDB.keys[dst] = srcDB.keys[src]
|
||||
destDB.incr(dst)
|
||||
if v, ok := srcDB.ttl[src]; ok {
|
||||
destDB.ttl[dst] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyHashKey(orig hashKey) hashKey {
|
||||
cpy := hashKey{}
|
||||
for k, v := range orig {
|
||||
cpy[k] = v
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
func copyListKey(orig listKey) listKey {
|
||||
cpy := make(listKey, len(orig))
|
||||
copy(cpy, orig)
|
||||
return cpy
|
||||
}
|
||||
|
||||
func copySetKey(orig setKey) setKey {
|
||||
cpy := setKey{}
|
||||
for k, v := range orig {
|
||||
cpy[k] = v
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
|
||||
func copySortedSet(orig sortedSet) sortedSet {
|
||||
cpy := sortedSet{}
|
||||
for k, v := range orig {
|
||||
cpy[k] = v
|
||||
}
|
||||
return cpy
|
||||
}
|
||||
Reference in New Issue
Block a user