Платформа ЦРНП "Мирокод" для разработки проектов
https://git.mirocod.ru
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
473 lines
9.8 KiB
473 lines
9.8 KiB
package redis |
|
|
|
import ( |
|
"errors" |
|
"fmt" |
|
"sync" |
|
"time" |
|
|
|
"github.com/go-redis/redis/internal" |
|
"github.com/go-redis/redis/internal/pool" |
|
"github.com/go-redis/redis/internal/proto" |
|
) |
|
|
|
var errPingTimeout = errors.New("redis: ping timeout") |
|
|
|
// PubSub implements Pub/Sub commands bas described in |
|
// http://redis.io/topics/pubsub. Message receiving is NOT safe |
|
// for concurrent use by multiple goroutines. |
|
// |
|
// PubSub automatically reconnects to Redis Server and resubscribes |
|
// to the channels in case of network errors. |
|
type PubSub struct { |
|
opt *Options |
|
|
|
newConn func([]string) (*pool.Conn, error) |
|
closeConn func(*pool.Conn) error |
|
|
|
mu sync.Mutex |
|
cn *pool.Conn |
|
channels map[string]struct{} |
|
patterns map[string]struct{} |
|
closed bool |
|
exit chan struct{} |
|
|
|
cmd *Cmd |
|
|
|
chOnce sync.Once |
|
ch chan *Message |
|
ping chan struct{} |
|
} |
|
|
|
func (c *PubSub) init() { |
|
c.exit = make(chan struct{}) |
|
} |
|
|
|
func (c *PubSub) conn() (*pool.Conn, error) { |
|
c.mu.Lock() |
|
cn, err := c._conn(nil) |
|
c.mu.Unlock() |
|
return cn, err |
|
} |
|
|
|
func (c *PubSub) _conn(newChannels []string) (*pool.Conn, error) { |
|
if c.closed { |
|
return nil, pool.ErrClosed |
|
} |
|
if c.cn != nil { |
|
return c.cn, nil |
|
} |
|
|
|
channels := mapKeys(c.channels) |
|
channels = append(channels, newChannels...) |
|
|
|
cn, err := c.newConn(channels) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
if err := c.resubscribe(cn); err != nil { |
|
_ = c.closeConn(cn) |
|
return nil, err |
|
} |
|
|
|
c.cn = cn |
|
return cn, nil |
|
} |
|
|
|
func (c *PubSub) writeCmd(cn *pool.Conn, cmd Cmder) error { |
|
return cn.WithWriter(c.opt.WriteTimeout, func(wr *proto.Writer) error { |
|
return writeCmd(wr, cmd) |
|
}) |
|
} |
|
|
|
func (c *PubSub) resubscribe(cn *pool.Conn) error { |
|
var firstErr error |
|
|
|
if len(c.channels) > 0 { |
|
err := c._subscribe(cn, "subscribe", mapKeys(c.channels)) |
|
if err != nil && firstErr == nil { |
|
firstErr = err |
|
} |
|
} |
|
|
|
if len(c.patterns) > 0 { |
|
err := c._subscribe(cn, "psubscribe", mapKeys(c.patterns)) |
|
if err != nil && firstErr == nil { |
|
firstErr = err |
|
} |
|
} |
|
|
|
return firstErr |
|
} |
|
|
|
func mapKeys(m map[string]struct{}) []string { |
|
s := make([]string, len(m)) |
|
i := 0 |
|
for k := range m { |
|
s[i] = k |
|
i++ |
|
} |
|
return s |
|
} |
|
|
|
func (c *PubSub) _subscribe( |
|
cn *pool.Conn, redisCmd string, channels []string, |
|
) error { |
|
args := make([]interface{}, 0, 1+len(channels)) |
|
args = append(args, redisCmd) |
|
for _, channel := range channels { |
|
args = append(args, channel) |
|
} |
|
cmd := NewSliceCmd(args...) |
|
return c.writeCmd(cn, cmd) |
|
} |
|
|
|
func (c *PubSub) releaseConn(cn *pool.Conn, err error, allowTimeout bool) { |
|
c.mu.Lock() |
|
c._releaseConn(cn, err, allowTimeout) |
|
c.mu.Unlock() |
|
} |
|
|
|
func (c *PubSub) _releaseConn(cn *pool.Conn, err error, allowTimeout bool) { |
|
if c.cn != cn { |
|
return |
|
} |
|
if internal.IsBadConn(err, allowTimeout) { |
|
c._reconnect(err) |
|
} |
|
} |
|
|
|
func (c *PubSub) _reconnect(reason error) { |
|
_ = c._closeTheCn(reason) |
|
_, _ = c._conn(nil) |
|
} |
|
|
|
func (c *PubSub) _closeTheCn(reason error) error { |
|
if c.cn == nil { |
|
return nil |
|
} |
|
if !c.closed { |
|
internal.Logf("redis: discarding bad PubSub connection: %s", reason) |
|
} |
|
err := c.closeConn(c.cn) |
|
c.cn = nil |
|
return err |
|
} |
|
|
|
func (c *PubSub) Close() error { |
|
c.mu.Lock() |
|
defer c.mu.Unlock() |
|
|
|
if c.closed { |
|
return pool.ErrClosed |
|
} |
|
c.closed = true |
|
close(c.exit) |
|
|
|
err := c._closeTheCn(pool.ErrClosed) |
|
return err |
|
} |
|
|
|
// Subscribe the client to the specified channels. It returns |
|
// empty subscription if there are no channels. |
|
func (c *PubSub) Subscribe(channels ...string) error { |
|
c.mu.Lock() |
|
defer c.mu.Unlock() |
|
|
|
err := c.subscribe("subscribe", channels...) |
|
if c.channels == nil { |
|
c.channels = make(map[string]struct{}) |
|
} |
|
for _, s := range channels { |
|
c.channels[s] = struct{}{} |
|
} |
|
return err |
|
} |
|
|
|
// PSubscribe the client to the given patterns. It returns |
|
// empty subscription if there are no patterns. |
|
func (c *PubSub) PSubscribe(patterns ...string) error { |
|
c.mu.Lock() |
|
defer c.mu.Unlock() |
|
|
|
err := c.subscribe("psubscribe", patterns...) |
|
if c.patterns == nil { |
|
c.patterns = make(map[string]struct{}) |
|
} |
|
for _, s := range patterns { |
|
c.patterns[s] = struct{}{} |
|
} |
|
return err |
|
} |
|
|
|
// Unsubscribe the client from the given channels, or from all of |
|
// them if none is given. |
|
func (c *PubSub) Unsubscribe(channels ...string) error { |
|
c.mu.Lock() |
|
defer c.mu.Unlock() |
|
|
|
for _, channel := range channels { |
|
delete(c.channels, channel) |
|
} |
|
err := c.subscribe("unsubscribe", channels...) |
|
return err |
|
} |
|
|
|
// PUnsubscribe the client from the given patterns, or from all of |
|
// them if none is given. |
|
func (c *PubSub) PUnsubscribe(patterns ...string) error { |
|
c.mu.Lock() |
|
defer c.mu.Unlock() |
|
|
|
for _, pattern := range patterns { |
|
delete(c.patterns, pattern) |
|
} |
|
err := c.subscribe("punsubscribe", patterns...) |
|
return err |
|
} |
|
|
|
func (c *PubSub) subscribe(redisCmd string, channels ...string) error { |
|
cn, err := c._conn(channels) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
err = c._subscribe(cn, redisCmd, channels) |
|
c._releaseConn(cn, err, false) |
|
return err |
|
} |
|
|
|
func (c *PubSub) Ping(payload ...string) error { |
|
args := []interface{}{"ping"} |
|
if len(payload) == 1 { |
|
args = append(args, payload[0]) |
|
} |
|
cmd := NewCmd(args...) |
|
|
|
cn, err := c.conn() |
|
if err != nil { |
|
return err |
|
} |
|
|
|
err = c.writeCmd(cn, cmd) |
|
c.releaseConn(cn, err, false) |
|
return err |
|
} |
|
|
|
// Subscription received after a successful subscription to channel. |
|
type Subscription struct { |
|
// Can be "subscribe", "unsubscribe", "psubscribe" or "punsubscribe". |
|
Kind string |
|
// Channel name we have subscribed to. |
|
Channel string |
|
// Number of channels we are currently subscribed to. |
|
Count int |
|
} |
|
|
|
func (m *Subscription) String() string { |
|
return fmt.Sprintf("%s: %s", m.Kind, m.Channel) |
|
} |
|
|
|
// Message received as result of a PUBLISH command issued by another client. |
|
type Message struct { |
|
Channel string |
|
Pattern string |
|
Payload string |
|
} |
|
|
|
func (m *Message) String() string { |
|
return fmt.Sprintf("Message<%s: %s>", m.Channel, m.Payload) |
|
} |
|
|
|
// Pong received as result of a PING command issued by another client. |
|
type Pong struct { |
|
Payload string |
|
} |
|
|
|
func (p *Pong) String() string { |
|
if p.Payload != "" { |
|
return fmt.Sprintf("Pong<%s>", p.Payload) |
|
} |
|
return "Pong" |
|
} |
|
|
|
func (c *PubSub) newMessage(reply interface{}) (interface{}, error) { |
|
switch reply := reply.(type) { |
|
case string: |
|
return &Pong{ |
|
Payload: reply, |
|
}, nil |
|
case []interface{}: |
|
switch kind := reply[0].(string); kind { |
|
case "subscribe", "unsubscribe", "psubscribe", "punsubscribe": |
|
return &Subscription{ |
|
Kind: kind, |
|
Channel: reply[1].(string), |
|
Count: int(reply[2].(int64)), |
|
}, nil |
|
case "message": |
|
return &Message{ |
|
Channel: reply[1].(string), |
|
Payload: reply[2].(string), |
|
}, nil |
|
case "pmessage": |
|
return &Message{ |
|
Pattern: reply[1].(string), |
|
Channel: reply[2].(string), |
|
Payload: reply[3].(string), |
|
}, nil |
|
case "pong": |
|
return &Pong{ |
|
Payload: reply[1].(string), |
|
}, nil |
|
default: |
|
return nil, fmt.Errorf("redis: unsupported pubsub message: %q", kind) |
|
} |
|
default: |
|
return nil, fmt.Errorf("redis: unsupported pubsub message: %#v", reply) |
|
} |
|
} |
|
|
|
// ReceiveTimeout acts like Receive but returns an error if message |
|
// is not received in time. This is low-level API and in most cases |
|
// Channel should be used instead. |
|
func (c *PubSub) ReceiveTimeout(timeout time.Duration) (interface{}, error) { |
|
if c.cmd == nil { |
|
c.cmd = NewCmd() |
|
} |
|
|
|
cn, err := c.conn() |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
err = cn.WithReader(timeout, func(rd *proto.Reader) error { |
|
return c.cmd.readReply(rd) |
|
}) |
|
|
|
c.releaseConn(cn, err, timeout > 0) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return c.newMessage(c.cmd.Val()) |
|
} |
|
|
|
// Receive returns a message as a Subscription, Message, Pong or error. |
|
// See PubSub example for details. This is low-level API and in most cases |
|
// Channel should be used instead. |
|
func (c *PubSub) Receive() (interface{}, error) { |
|
return c.ReceiveTimeout(0) |
|
} |
|
|
|
// ReceiveMessage returns a Message or error ignoring Subscription and Pong |
|
// messages. This is low-level API and in most cases Channel should be used |
|
// instead. |
|
func (c *PubSub) ReceiveMessage() (*Message, error) { |
|
for { |
|
msg, err := c.Receive() |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
switch msg := msg.(type) { |
|
case *Subscription: |
|
// Ignore. |
|
case *Pong: |
|
// Ignore. |
|
case *Message: |
|
return msg, nil |
|
default: |
|
err := fmt.Errorf("redis: unknown message: %T", msg) |
|
return nil, err |
|
} |
|
} |
|
} |
|
|
|
// Channel returns a Go channel for concurrently receiving messages. |
|
// It periodically sends Ping messages to test connection health. |
|
// The channel is closed with PubSub. Receive* APIs can not be used |
|
// after channel is created. |
|
func (c *PubSub) Channel() <-chan *Message { |
|
c.chOnce.Do(c.initChannel) |
|
return c.ch |
|
} |
|
|
|
func (c *PubSub) initChannel() { |
|
c.ch = make(chan *Message, 100) |
|
c.ping = make(chan struct{}, 10) |
|
|
|
go func() { |
|
var errCount int |
|
for { |
|
msg, err := c.Receive() |
|
if err != nil { |
|
if err == pool.ErrClosed { |
|
close(c.ch) |
|
return |
|
} |
|
if errCount > 0 { |
|
time.Sleep(c.retryBackoff(errCount)) |
|
} |
|
errCount++ |
|
continue |
|
} |
|
errCount = 0 |
|
|
|
// Any message is as good as a ping. |
|
select { |
|
case c.ping <- struct{}{}: |
|
default: |
|
} |
|
|
|
switch msg := msg.(type) { |
|
case *Subscription: |
|
// Ignore. |
|
case *Pong: |
|
// Ignore. |
|
case *Message: |
|
c.ch <- msg |
|
default: |
|
internal.Logf("redis: unknown message: %T", msg) |
|
} |
|
} |
|
}() |
|
|
|
go func() { |
|
const timeout = 5 * time.Second |
|
|
|
timer := time.NewTimer(timeout) |
|
timer.Stop() |
|
|
|
healthy := true |
|
for { |
|
timer.Reset(timeout) |
|
select { |
|
case <-c.ping: |
|
healthy = true |
|
if !timer.Stop() { |
|
<-timer.C |
|
} |
|
case <-timer.C: |
|
pingErr := c.Ping() |
|
if healthy { |
|
healthy = false |
|
} else { |
|
if pingErr == nil { |
|
pingErr = errPingTimeout |
|
} |
|
c.mu.Lock() |
|
c._reconnect(pingErr) |
|
c.mu.Unlock() |
|
} |
|
case <-c.exit: |
|
return |
|
} |
|
} |
|
}() |
|
} |
|
|
|
func (c *PubSub) retryBackoff(attempt int) time.Duration { |
|
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff) |
|
}
|
|
|