Enable event persistence in Redis (#7601)

This commit is contained in:
Praveen raj Mani 2019-07-23 22:52:08 +05:30 committed by kannappanr
parent 9389a55e5d
commit 2b9b907f9c
10 changed files with 185 additions and 83 deletions

View File

@ -192,7 +192,9 @@ var (
"format": "namespace", "format": "namespace",
"address": "", "address": "",
"password": "", "password": "",
"key": "" "key": "",
"queueDir": "",
"queueLimit": 0
} }
}, },
"webhook": { "webhook": {

View File

@ -391,7 +391,7 @@ func (s *serverConfig) TestNotificationTargets() error {
if !v.Enable { if !v.Enable {
continue continue
} }
t, err := target.NewRedisTarget(k, v) t, err := target.NewRedisTarget(k, v, GlobalServiceDoneCh)
if err != nil { if err != nil {
return fmt.Errorf("redis(%s): %s", k, err.Error()) return fmt.Errorf("redis(%s): %s", k, err.Error())
} }
@ -752,7 +752,7 @@ func getNotificationTargets(config *serverConfig) *event.TargetList {
for id, args := range config.Notify.Redis { for id, args := range config.Notify.Redis {
if args.Enable { if args.Enable {
newTarget, err := target.NewRedisTarget(id, args) newTarget, err := target.NewRedisTarget(id, args, GlobalServiceDoneCh)
if err != nil { if err != nil {
logger.LogIf(context.Background(), err) logger.LogIf(context.Background(), err)
continue continue

View File

@ -227,7 +227,7 @@ func TestValidateConfig(t *testing.T) {
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex", "queueDir": "", "queueLimit": 0 } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "elasticsearch": { "1": { "enable": true, "format": "namespace", "url": "example.com", "index": "myindex", "queueDir": "", "queueLimit": 0 } }}}`, true},
// Test 25 - Test Format for Redis // Test 25 - Test Format for Redis
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, false}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "invalid", "address": "example.com:80", "password": "xxx", "key": "key1", "queueDir": "", "queueLimit": 0 } }}}`, false},
// Test 26 - Test valid Format for Redis // Test 26 - Test valid Format for Redis
{`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true}, {`{"version": "` + v + `", "credential": { "accessKey": "minio", "secretKey": "minio123" }, "region": "us-east-1", "browser": "on", "notify": { "redis": { "1": { "enable": true, "format": "namespace", "address": "example.com:80", "password": "xxx", "key": "key1" } }}}`, true},

BIN
dockerscripts/check-user Executable file

Binary file not shown.

View File

@ -460,11 +460,15 @@ An example of Redis configuration is as follows:
"format": "namespace", "format": "namespace",
"address": "127.0.0.1:6379", "address": "127.0.0.1:6379",
"password": "yoursecret", "password": "yoursecret",
"key": "bucketevents" "key": "bucketevents",
"queueDir": "",
"queueLimit": 0
} }
} }
``` ```
MinIO supports persistent event store. The persistent store will backup events when the Redis broker goes offline and replays it when the broker comes back online. The event store can be configured by setting the directory path in `queueDir` field and the maximum limit of events in the queueDir in `queueLimit` field. For eg, the `queueDir` can be `/home/events` and `queueLimit` can be `1000`. By default, the `queueLimit` is set to 10000.
To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally. To update the configuration, use `mc admin config get` command to get the current configuration file for the minio deployment in json format, and save it locally.
```sh ```sh

View File

@ -157,7 +157,9 @@
"format": "", "format": "",
"address": "", "address": "",
"password": "", "password": "",
"key": "" "key": "",
"queueDir": "",
"queueLimit": 0
} }
}, },
"webhook": { "webhook": {

View File

@ -20,11 +20,9 @@ import (
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"errors" "errors"
"net"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"syscall"
"github.com/nsqio/go-nsq" "github.com/nsqio/go-nsq"
@ -90,7 +88,7 @@ func (target *NSQTarget) Save(eventData event.Event) error {
} }
if err := target.producer.Ping(); err != nil { if err := target.producer.Ping(); err != nil {
// To treat "connection refused" errors as errNotConnected. // To treat "connection refused" errors as errNotConnected.
if isConnRefusedErr(err) { if IsConnRefusedErr(err) {
return errNotConnected return errNotConnected
} }
return err return err
@ -98,20 +96,6 @@ func (target *NSQTarget) Save(eventData event.Event) error {
return target.send(eventData) return target.send(eventData)
} }
// isConnRefusedErr - To check fot "connection refused" error.
func isConnRefusedErr(err error) bool {
if opErr, ok := err.(*net.OpError); ok {
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
if errno, ok := sysErr.Err.(syscall.Errno); ok {
if errno == syscall.ECONNREFUSED {
return true
}
}
}
}
return false
}
// send - sends an event to the NSQ. // send - sends an event to the NSQ.
func (target *NSQTarget) send(eventData event.Event) error { func (target *NSQTarget) send(eventData event.Event) error {
objectName, err := url.QueryUnescape(eventData.S3.Object.Key) objectName, err := url.QueryUnescape(eventData.S3.Object.Key)
@ -133,7 +117,7 @@ func (target *NSQTarget) Send(eventKey string) error {
if err := target.producer.Ping(); err != nil { if err := target.producer.Ping(); err != nil {
// To treat "connection refused" errors as errNotConnected. // To treat "connection refused" errors as errNotConnected.
if isConnRefusedErr(err) { if IsConnRefusedErr(err) {
return errNotConnected return errNotConnected
} }
return err return err
@ -198,7 +182,7 @@ func NewNSQTarget(id string, args NSQArgs, doneCh <-chan struct{}) (*NSQTarget,
if err := target.producer.Ping(); err != nil { if err := target.producer.Ping(); err != nil {
// To treat "connection refused" errors as errNotConnected. // To treat "connection refused" errors as errNotConnected.
if target.store == nil || !isConnRefusedErr(err) { if target.store == nil || !IsConnRefusedErr(err) {
return nil, err return nil, err
} }
} }

View File

@ -17,13 +17,18 @@
package target package target
import ( import (
"context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/url" "net/url"
"os"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/gomodule/redigo/redis" "github.com/gomodule/redigo/redis"
"github.com/minio/minio/cmd/logger"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
xnet "github.com/minio/minio/pkg/net" xnet "github.com/minio/minio/pkg/net"
) )
@ -35,6 +40,8 @@ type RedisArgs struct {
Addr xnet.Host `json:"address"` Addr xnet.Host `json:"address"`
Password string `json:"password"` Password string `json:"password"`
Key string `json:"key"` Key string `json:"key"`
QueueDir string `json:"queueDir"`
QueueLimit uint64 `json:"queueLimit"`
} }
// Validate RedisArgs fields // Validate RedisArgs fields
@ -54,6 +61,35 @@ func (r RedisArgs) Validate() error {
return fmt.Errorf("empty key") return fmt.Errorf("empty key")
} }
if r.QueueDir != "" {
if !filepath.IsAbs(r.QueueDir) {
return errors.New("queueDir path should be absolute")
}
}
if r.QueueLimit > 10000 {
return errors.New("queueLimit should not exceed 10000")
}
return nil
}
func (r RedisArgs) validateFormat(c redis.Conn) error {
typeAvailable, err := redis.String(c.Do("TYPE", r.Key))
if err != nil {
return err
}
if typeAvailable != "none" {
expectedType := "hash"
if r.Format == event.AccessFormat {
expectedType = "list"
}
if typeAvailable != expectedType {
return fmt.Errorf("expected type %v does not match with available type %v", expectedType, typeAvailable)
}
}
return nil return nil
} }
@ -62,6 +98,8 @@ type RedisTarget struct {
id event.TargetID id event.TargetID
args RedisArgs args RedisArgs
pool *redis.Pool pool *redis.Pool
store Store
firstPing bool
} }
// ID - returns target ID. // ID - returns target ID.
@ -69,16 +107,32 @@ func (target *RedisTarget) ID() event.TargetID {
return target.id return target.id
} }
// Save - Sends event directly without persisting. // Save - saves the events to the store if questore is configured, which will be replayed when the redis connection is active.
func (target *RedisTarget) Save(eventData event.Event) error { func (target *RedisTarget) Save(eventData event.Event) error {
if target.store != nil {
return target.store.Put(eventData)
}
conn := target.pool.Get()
defer func() {
cErr := conn.Close()
logger.LogOnceIf(context.Background(), cErr, target.ID())
}()
_, pingErr := conn.Do("PING")
if pingErr != nil {
if IsConnRefusedErr(pingErr) {
return errNotConnected
}
return pingErr
}
return target.send(eventData) return target.send(eventData)
} }
// send - sends an event to the redis.
func (target *RedisTarget) send(eventData event.Event) error { func (target *RedisTarget) send(eventData event.Event) error {
conn := target.pool.Get() conn := target.pool.Get()
defer func() { defer func() {
// FIXME: log returned error. ignore time being. cErr := conn.Close()
_ = conn.Close() logger.LogOnceIf(context.Background(), cErr, target.ID())
}() }()
if target.args.Format == event.NamespaceFormat { if target.args.Format == event.NamespaceFormat {
@ -98,24 +152,68 @@ func (target *RedisTarget) send(eventData event.Event) error {
_, err = conn.Do("HSET", target.args.Key, key, data) _, err = conn.Do("HSET", target.args.Key, key, data)
} }
if err != nil {
return err return err
} }
}
if target.args.Format == event.AccessFormat { if target.args.Format == event.AccessFormat {
data, err := json.Marshal([]interface{}{eventData.EventTime, []event.Event{eventData}}) data, err := json.Marshal([]interface{}{eventData.EventTime, []event.Event{eventData}})
if err != nil { if err != nil {
return err return err
} }
_, err = conn.Do("RPUSH", target.args.Key, data) if _, err := conn.Do("RPUSH", target.args.Key, data); err != nil {
return err return err
} }
}
return nil return nil
} }
// Send - interface compatible method does no-op. // Send - reads an event from store and sends it to redis.
func (target *RedisTarget) Send(eventKey string) error { func (target *RedisTarget) Send(eventKey string) error {
conn := target.pool.Get()
defer func() {
cErr := conn.Close()
logger.LogOnceIf(context.Background(), cErr, target.ID())
}()
_, pingErr := conn.Do("PING")
if pingErr != nil {
if IsConnRefusedErr(pingErr) {
return errNotConnected
}
return pingErr
}
if !target.firstPing {
if err := target.args.validateFormat(conn); err != nil {
if IsConnRefusedErr(err) {
return errNotConnected
}
return err
}
target.firstPing = true
}
eventData, eErr := target.store.Get(eventKey)
if eErr != nil {
// The last event key in a successful batch will be sent in the channel atmost once by the replayEvents()
// Such events will not exist and would've been already been sent successfully.
if os.IsNotExist(eErr) {
return nil return nil
}
return eErr
}
if err := target.send(eventData); err != nil {
if IsConnRefusedErr(err) {
return errNotConnected
}
return err
}
// Delete the event from store.
return target.store.Del(eventKey)
} }
// Close - does nothing and available for interface compatibility. // Close - does nothing and available for interface compatibility.
@ -124,7 +222,7 @@ func (target *RedisTarget) Close() error {
} }
// NewRedisTarget - creates new Redis target. // NewRedisTarget - creates new Redis target.
func NewRedisTarget(id string, args RedisArgs) (*RedisTarget, error) { func NewRedisTarget(id string, args RedisArgs, doneCh <-chan struct{}) (*RedisTarget, error) {
pool := &redis.Pool{ pool := &redis.Pool{
MaxIdle: 3, MaxIdle: 3,
IdleTimeout: 2 * 60 * time.Second, IdleTimeout: 2 * 60 * time.Second,
@ -139,8 +237,9 @@ func NewRedisTarget(id string, args RedisArgs) (*RedisTarget, error) {
} }
if _, err = conn.Do("AUTH", args.Password); err != nil { if _, err = conn.Do("AUTH", args.Password); err != nil {
// FIXME: log returned error. ignore time being. cErr := conn.Close()
_ = conn.Close() targetID := event.TargetID{ID: id, Name: "redis"}
logger.LogOnceIf(context.Background(), cErr, targetID.String())
return nil, err return nil, err
} }
@ -152,35 +251,47 @@ func NewRedisTarget(id string, args RedisArgs) (*RedisTarget, error) {
}, },
} }
conn := pool.Get() var store Store
defer func() {
// FIXME: log returned error. ignore time being.
_ = conn.Close()
}()
if _, err := conn.Do("PING"); err != nil { if args.QueueDir != "" {
return nil, err queueDir := filepath.Join(args.QueueDir, storePrefix+"-redis-"+id)
} store = NewQueueStore(queueDir, args.QueueLimit)
if oErr := store.Open(); oErr != nil {
typeAvailable, err := redis.String(conn.Do("TYPE", args.Key)) return nil, oErr
if err != nil {
return nil, err
}
if typeAvailable != "none" {
expectedType := "hash"
if args.Format == event.AccessFormat {
expectedType = "list"
}
if typeAvailable != expectedType {
return nil, fmt.Errorf("expected type %v does not match with available type %v", expectedType, typeAvailable)
} }
} }
return &RedisTarget{ target := &RedisTarget{
id: event.TargetID{ID: id, Name: "redis"}, id: event.TargetID{ID: id, Name: "redis"},
args: args, args: args,
pool: pool, pool: pool,
}, nil store: store,
}
conn := target.pool.Get()
defer func() {
cErr := conn.Close()
logger.LogOnceIf(context.Background(), cErr, target.ID())
}()
_, pingErr := conn.Do("PING")
if pingErr != nil {
if target.store == nil || !IsConnRefusedErr(pingErr) {
return nil, pingErr
}
} else {
if err := target.args.validateFormat(conn); err != nil {
return nil, err
}
target.firstPing = true
}
if target.store != nil {
// Replays the events from the store.
eventKeyCh := replayEvents(target.store, doneCh)
// Start replaying events from the store.
go sendEvents(target, eventKeyCh, doneCh)
}
return target, nil
} }

View File

@ -79,6 +79,20 @@ func replayEvents(store Store, doneCh <-chan struct{}) <-chan string {
return eventKeyCh return eventKeyCh
} }
// IsConnRefusedErr - To check fot "connection refused" error.
func IsConnRefusedErr(err error) bool {
if opErr, ok := err.(*net.OpError); ok {
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
if errno, ok := sysErr.Err.(syscall.Errno); ok {
if errno == syscall.ECONNREFUSED {
return true
}
}
}
}
return false
}
// isConnResetErr - Checks for connection reset errors. // isConnResetErr - Checks for connection reset errors.
func isConnResetErr(err error) bool { func isConnResetErr(err error) bool {
if opErr, ok := err.(*net.OpError); ok { if opErr, ok := err.(*net.OpError); ok {

View File

@ -30,7 +30,6 @@ import (
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"syscall"
"time" "time"
"github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event"
@ -134,20 +133,6 @@ func (target *WebhookTarget) send(eventData event.Event) error {
return nil return nil
} }
// IsConnRefusedErr - To check for "connection refused" errors.
func IsConnRefusedErr(err error) bool {
if opErr, ok := err.(*net.OpError); ok {
if sysErr, ok := opErr.Err.(*os.SyscallError); ok {
if errno, ok := sysErr.Err.(syscall.Errno); ok {
if errno == syscall.ECONNREFUSED {
return true
}
}
}
}
return false
}
// Send - reads an event from store and sends it to webhook. // Send - reads an event from store and sends it to webhook.
func (target *WebhookTarget) Send(eventKey string) error { func (target *WebhookTarget) Send(eventKey string) error {