mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-10-14 04:07:46 +00:00
* 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>
760 lines
18 KiB
Go
760 lines
18 KiB
Go
// 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
|
|
}
|