github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/redis.go (about) 1 // Copyright (c) 2015-2023 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package target 19 20 import ( 21 "context" 22 "encoding/json" 23 "errors" 24 "fmt" 25 "net/url" 26 "os" 27 "path/filepath" 28 "strings" 29 "time" 30 31 "github.com/gomodule/redigo/redis" 32 "github.com/minio/minio/internal/event" 33 "github.com/minio/minio/internal/logger" 34 "github.com/minio/minio/internal/once" 35 "github.com/minio/minio/internal/store" 36 xnet "github.com/minio/pkg/v2/net" 37 ) 38 39 // Redis constants 40 const ( 41 RedisFormat = "format" 42 RedisAddress = "address" 43 RedisPassword = "password" 44 RedisUser = "user" 45 RedisKey = "key" 46 RedisQueueDir = "queue_dir" 47 RedisQueueLimit = "queue_limit" 48 49 EnvRedisEnable = "MINIO_NOTIFY_REDIS_ENABLE" 50 EnvRedisFormat = "MINIO_NOTIFY_REDIS_FORMAT" 51 EnvRedisAddress = "MINIO_NOTIFY_REDIS_ADDRESS" 52 EnvRedisPassword = "MINIO_NOTIFY_REDIS_PASSWORD" 53 EnvRedisUser = "MINIO_NOTIFY_REDIS_USER" 54 EnvRedisKey = "MINIO_NOTIFY_REDIS_KEY" 55 EnvRedisQueueDir = "MINIO_NOTIFY_REDIS_QUEUE_DIR" 56 EnvRedisQueueLimit = "MINIO_NOTIFY_REDIS_QUEUE_LIMIT" 57 ) 58 59 // RedisArgs - Redis target arguments. 60 type RedisArgs struct { 61 Enable bool `json:"enable"` 62 Format string `json:"format"` 63 Addr xnet.Host `json:"address"` 64 Password string `json:"password"` 65 User string `json:"user"` 66 Key string `json:"key"` 67 QueueDir string `json:"queueDir"` 68 QueueLimit uint64 `json:"queueLimit"` 69 } 70 71 // RedisAccessEvent holds event log data and timestamp 72 type RedisAccessEvent struct { 73 Event []event.Event 74 EventTime string 75 } 76 77 // Validate RedisArgs fields 78 func (r RedisArgs) Validate() error { 79 if !r.Enable { 80 return nil 81 } 82 83 if r.Format != "" { 84 f := strings.ToLower(r.Format) 85 if f != event.NamespaceFormat && f != event.AccessFormat { 86 return fmt.Errorf("unrecognized format") 87 } 88 } 89 90 if r.Key == "" { 91 return fmt.Errorf("empty key") 92 } 93 94 if r.QueueDir != "" { 95 if !filepath.IsAbs(r.QueueDir) { 96 return errors.New("queueDir path should be absolute") 97 } 98 } 99 100 return nil 101 } 102 103 func (r RedisArgs) validateFormat(c redis.Conn) error { 104 typeAvailable, err := redis.String(c.Do("TYPE", r.Key)) 105 if err != nil { 106 return err 107 } 108 109 if typeAvailable != "none" { 110 expectedType := "hash" 111 if r.Format == event.AccessFormat { 112 expectedType = "list" 113 } 114 115 if typeAvailable != expectedType { 116 return fmt.Errorf("expected type %v does not match with available type %v", expectedType, typeAvailable) 117 } 118 } 119 120 return nil 121 } 122 123 // RedisTarget - Redis target. 124 type RedisTarget struct { 125 initOnce once.Init 126 127 id event.TargetID 128 args RedisArgs 129 pool *redis.Pool 130 store store.Store[event.Event] 131 firstPing bool 132 loggerOnce logger.LogOnce 133 quitCh chan struct{} 134 } 135 136 // ID - returns target ID. 137 func (target *RedisTarget) ID() event.TargetID { 138 return target.id 139 } 140 141 // Name - returns the Name of the target. 142 func (target *RedisTarget) Name() string { 143 return target.ID().String() 144 } 145 146 // Store returns any underlying store if set. 147 func (target *RedisTarget) Store() event.TargetStore { 148 return target.store 149 } 150 151 // IsActive - Return true if target is up and active 152 func (target *RedisTarget) IsActive() (bool, error) { 153 if err := target.init(); err != nil { 154 return false, err 155 } 156 return target.isActive() 157 } 158 159 func (target *RedisTarget) isActive() (bool, error) { 160 conn := target.pool.Get() 161 defer conn.Close() 162 163 _, pingErr := conn.Do("PING") 164 if pingErr != nil { 165 if xnet.IsConnRefusedErr(pingErr) { 166 return false, store.ErrNotConnected 167 } 168 return false, pingErr 169 } 170 return true, nil 171 } 172 173 // Save - saves the events to the store if questore is configured, which will be replayed when the redis connection is active. 174 func (target *RedisTarget) Save(eventData event.Event) error { 175 if target.store != nil { 176 return target.store.Put(eventData) 177 } 178 if err := target.init(); err != nil { 179 return err 180 } 181 _, err := target.isActive() 182 if err != nil { 183 return err 184 } 185 return target.send(eventData) 186 } 187 188 // send - sends an event to the redis. 189 func (target *RedisTarget) send(eventData event.Event) error { 190 conn := target.pool.Get() 191 defer conn.Close() 192 193 if target.args.Format == event.NamespaceFormat { 194 objectName, err := url.QueryUnescape(eventData.S3.Object.Key) 195 if err != nil { 196 return err 197 } 198 key := eventData.S3.Bucket.Name + "/" + objectName 199 200 if eventData.EventName == event.ObjectRemovedDelete { 201 _, err = conn.Do("HDEL", target.args.Key, key) 202 } else { 203 var data []byte 204 if data, err = json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}}); err != nil { 205 return err 206 } 207 208 _, err = conn.Do("HSET", target.args.Key, key, data) 209 } 210 if err != nil { 211 return err 212 } 213 } 214 215 if target.args.Format == event.AccessFormat { 216 data, err := json.Marshal([]RedisAccessEvent{{Event: []event.Event{eventData}, EventTime: eventData.EventTime}}) 217 if err != nil { 218 return err 219 } 220 if _, err := conn.Do("RPUSH", target.args.Key, data); err != nil { 221 return err 222 } 223 } 224 225 return nil 226 } 227 228 // SendFromStore - reads an event from store and sends it to redis. 229 func (target *RedisTarget) SendFromStore(key store.Key) error { 230 if err := target.init(); err != nil { 231 return err 232 } 233 234 conn := target.pool.Get() 235 defer conn.Close() 236 237 _, pingErr := conn.Do("PING") 238 if pingErr != nil { 239 if xnet.IsConnRefusedErr(pingErr) { 240 return store.ErrNotConnected 241 } 242 return pingErr 243 } 244 245 if !target.firstPing { 246 if err := target.args.validateFormat(conn); err != nil { 247 if xnet.IsConnRefusedErr(err) { 248 return store.ErrNotConnected 249 } 250 return err 251 } 252 target.firstPing = true 253 } 254 255 eventData, eErr := target.store.Get(key.Name) 256 if eErr != nil { 257 // The last event key in a successful batch will be sent in the channel atmost once by the replayEvents() 258 // Such events will not exist and would've been already been sent successfully. 259 if os.IsNotExist(eErr) { 260 return nil 261 } 262 return eErr 263 } 264 265 if err := target.send(eventData); err != nil { 266 if xnet.IsConnRefusedErr(err) { 267 return store.ErrNotConnected 268 } 269 return err 270 } 271 272 // Delete the event from store. 273 return target.store.Del(key.Name) 274 } 275 276 // Close - releases the resources used by the pool. 277 func (target *RedisTarget) Close() error { 278 close(target.quitCh) 279 if target.pool != nil { 280 return target.pool.Close() 281 } 282 return nil 283 } 284 285 func (target *RedisTarget) init() error { 286 return target.initOnce.Do(target.initRedis) 287 } 288 289 func (target *RedisTarget) initRedis() error { 290 conn := target.pool.Get() 291 defer conn.Close() 292 293 _, pingErr := conn.Do("PING") 294 if pingErr != nil { 295 if !(xnet.IsConnRefusedErr(pingErr) || xnet.IsConnResetErr(pingErr)) { 296 target.loggerOnce(context.Background(), pingErr, target.ID().String()) 297 } 298 return pingErr 299 } 300 301 if err := target.args.validateFormat(conn); err != nil { 302 target.loggerOnce(context.Background(), err, target.ID().String()) 303 return err 304 } 305 306 target.firstPing = true 307 308 yes, err := target.isActive() 309 if err != nil { 310 return err 311 } 312 if !yes { 313 return store.ErrNotConnected 314 } 315 316 return nil 317 } 318 319 // NewRedisTarget - creates new Redis target. 320 func NewRedisTarget(id string, args RedisArgs, loggerOnce logger.LogOnce) (*RedisTarget, error) { 321 var queueStore store.Store[event.Event] 322 if args.QueueDir != "" { 323 queueDir := filepath.Join(args.QueueDir, storePrefix+"-redis-"+id) 324 queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension) 325 if err := queueStore.Open(); err != nil { 326 return nil, fmt.Errorf("unable to initialize the queue store of Redis `%s`: %w", id, err) 327 } 328 } 329 330 pool := &redis.Pool{ 331 MaxIdle: 3, 332 IdleTimeout: 2 * 60 * time.Second, 333 Dial: func() (redis.Conn, error) { 334 conn, err := redis.Dial("tcp", args.Addr.String()) 335 if err != nil { 336 return nil, err 337 } 338 339 if args.Password != "" { 340 if args.User != "" { 341 if _, err = conn.Do("AUTH", args.User, args.Password); err != nil { 342 conn.Close() 343 return nil, err 344 } 345 } else { 346 if _, err = conn.Do("AUTH", args.Password); err != nil { 347 conn.Close() 348 return nil, err 349 } 350 } 351 } 352 353 // Must be done after AUTH 354 if _, err = conn.Do("CLIENT", "SETNAME", "MinIO"); err != nil { 355 conn.Close() 356 return nil, err 357 } 358 359 return conn, nil 360 }, 361 TestOnBorrow: func(c redis.Conn, t time.Time) error { 362 _, err := c.Do("PING") 363 return err 364 }, 365 } 366 367 target := &RedisTarget{ 368 id: event.TargetID{ID: id, Name: "redis"}, 369 args: args, 370 pool: pool, 371 store: queueStore, 372 loggerOnce: loggerOnce, 373 quitCh: make(chan struct{}), 374 } 375 376 if target.store != nil { 377 store.StreamItems(target.store, target, target.quitCh, target.loggerOnce) 378 } 379 380 return target, nil 381 }