storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/event/target/redis.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2018 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package target 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/url" 25 "os" 26 "path/filepath" 27 "strings" 28 "time" 29 30 "github.com/gomodule/redigo/redis" 31 32 "storj.io/minio/pkg/event" 33 xnet "storj.io/minio/pkg/net" 34 ) 35 36 // Redis constants 37 const ( 38 RedisFormat = "format" 39 RedisAddress = "address" 40 RedisPassword = "password" 41 RedisKey = "key" 42 RedisQueueDir = "queue_dir" 43 RedisQueueLimit = "queue_limit" 44 45 EnvRedisEnable = "MINIO_NOTIFY_REDIS_ENABLE" 46 EnvRedisFormat = "MINIO_NOTIFY_REDIS_FORMAT" 47 EnvRedisAddress = "MINIO_NOTIFY_REDIS_ADDRESS" 48 EnvRedisPassword = "MINIO_NOTIFY_REDIS_PASSWORD" 49 EnvRedisKey = "MINIO_NOTIFY_REDIS_KEY" 50 EnvRedisQueueDir = "MINIO_NOTIFY_REDIS_QUEUE_DIR" 51 EnvRedisQueueLimit = "MINIO_NOTIFY_REDIS_QUEUE_LIMIT" 52 ) 53 54 // RedisArgs - Redis target arguments. 55 type RedisArgs struct { 56 Enable bool `json:"enable"` 57 Format string `json:"format"` 58 Addr xnet.Host `json:"address"` 59 Password string `json:"password"` 60 Key string `json:"key"` 61 QueueDir string `json:"queueDir"` 62 QueueLimit uint64 `json:"queueLimit"` 63 } 64 65 // RedisAccessEvent holds event log data and timestamp 66 type RedisAccessEvent struct { 67 Event []event.Event 68 EventTime string 69 } 70 71 // Validate RedisArgs fields 72 func (r RedisArgs) Validate() error { 73 if !r.Enable { 74 return nil 75 } 76 77 if r.Format != "" { 78 f := strings.ToLower(r.Format) 79 if f != event.NamespaceFormat && f != event.AccessFormat { 80 return fmt.Errorf("unrecognized format") 81 } 82 } 83 84 if r.Key == "" { 85 return fmt.Errorf("empty key") 86 } 87 88 if r.QueueDir != "" { 89 if !filepath.IsAbs(r.QueueDir) { 90 return errors.New("queueDir path should be absolute") 91 } 92 } 93 94 return nil 95 } 96 97 func (r RedisArgs) validateFormat(c redis.Conn) error { 98 typeAvailable, err := redis.String(c.Do("TYPE", r.Key)) 99 if err != nil { 100 return err 101 } 102 103 if typeAvailable != "none" { 104 expectedType := "hash" 105 if r.Format == event.AccessFormat { 106 expectedType = "list" 107 } 108 109 if typeAvailable != expectedType { 110 return fmt.Errorf("expected type %v does not match with available type %v", expectedType, typeAvailable) 111 } 112 } 113 114 return nil 115 } 116 117 // RedisTarget - Redis target. 118 type RedisTarget struct { 119 id event.TargetID 120 args RedisArgs 121 pool *redis.Pool 122 store Store 123 firstPing bool 124 loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}) 125 } 126 127 // ID - returns target ID. 128 func (target *RedisTarget) ID() event.TargetID { 129 return target.id 130 } 131 132 // HasQueueStore - Checks if the queueStore has been configured for the target 133 func (target *RedisTarget) HasQueueStore() bool { 134 return target.store != nil 135 } 136 137 // IsActive - Return true if target is up and active 138 func (target *RedisTarget) IsActive() (bool, error) { 139 conn := target.pool.Get() 140 defer func() { 141 cErr := conn.Close() 142 target.loggerOnce(context.Background(), cErr, target.ID()) 143 }() 144 _, pingErr := conn.Do("PING") 145 if pingErr != nil { 146 if IsConnRefusedErr(pingErr) { 147 return false, errNotConnected 148 } 149 return false, pingErr 150 } 151 return true, nil 152 } 153 154 // Save - saves the events to the store if questore is configured, which will be replayed when the redis connection is active. 155 func (target *RedisTarget) Save(eventData event.Event) error { 156 if target.store != nil { 157 return target.store.Put(eventData) 158 } 159 _, err := target.IsActive() 160 if err != nil { 161 return err 162 } 163 return target.send(eventData) 164 } 165 166 // send - sends an event to the redis. 167 func (target *RedisTarget) send(eventData event.Event) error { 168 conn := target.pool.Get() 169 defer func() { 170 cErr := conn.Close() 171 target.loggerOnce(context.Background(), cErr, target.ID()) 172 }() 173 174 if target.args.Format == event.NamespaceFormat { 175 objectName, err := url.QueryUnescape(eventData.S3.Object.Key) 176 if err != nil { 177 return err 178 } 179 key := eventData.S3.Bucket.Name + "/" + objectName 180 181 if eventData.EventName == event.ObjectRemovedDelete { 182 _, err = conn.Do("HDEL", target.args.Key, key) 183 } else { 184 var data []byte 185 if data, err = json.Marshal(struct{ Records []event.Event }{[]event.Event{eventData}}); err != nil { 186 return err 187 } 188 189 _, err = conn.Do("HSET", target.args.Key, key, data) 190 } 191 if err != nil { 192 return err 193 } 194 } 195 196 if target.args.Format == event.AccessFormat { 197 data, err := json.Marshal([]RedisAccessEvent{{Event: []event.Event{eventData}, EventTime: eventData.EventTime}}) 198 if err != nil { 199 return err 200 } 201 if _, err := conn.Do("RPUSH", target.args.Key, data); err != nil { 202 return err 203 } 204 } 205 206 return nil 207 } 208 209 // Send - reads an event from store and sends it to redis. 210 func (target *RedisTarget) Send(eventKey string) error { 211 conn := target.pool.Get() 212 defer func() { 213 cErr := conn.Close() 214 target.loggerOnce(context.Background(), cErr, target.ID()) 215 }() 216 _, pingErr := conn.Do("PING") 217 if pingErr != nil { 218 if IsConnRefusedErr(pingErr) { 219 return errNotConnected 220 } 221 return pingErr 222 } 223 224 if !target.firstPing { 225 if err := target.args.validateFormat(conn); err != nil { 226 if IsConnRefusedErr(err) { 227 return errNotConnected 228 } 229 return err 230 } 231 target.firstPing = true 232 } 233 234 eventData, eErr := target.store.Get(eventKey) 235 if eErr != nil { 236 // The last event key in a successful batch will be sent in the channel atmost once by the replayEvents() 237 // Such events will not exist and would've been already been sent successfully. 238 if os.IsNotExist(eErr) { 239 return nil 240 } 241 return eErr 242 } 243 244 if err := target.send(eventData); err != nil { 245 if IsConnRefusedErr(err) { 246 return errNotConnected 247 } 248 return err 249 } 250 251 // Delete the event from store. 252 return target.store.Del(eventKey) 253 } 254 255 // Close - releases the resources used by the pool. 256 func (target *RedisTarget) Close() error { 257 return target.pool.Close() 258 } 259 260 // NewRedisTarget - creates new Redis target. 261 func NewRedisTarget(id string, args RedisArgs, doneCh <-chan struct{}, loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}), test bool) (*RedisTarget, error) { 262 pool := &redis.Pool{ 263 MaxIdle: 3, 264 IdleTimeout: 2 * 60 * time.Second, 265 Dial: func() (redis.Conn, error) { 266 conn, err := redis.Dial("tcp", args.Addr.String()) 267 if err != nil { 268 return nil, err 269 } 270 271 if args.Password != "" { 272 if _, err = conn.Do("AUTH", args.Password); err != nil { 273 cErr := conn.Close() 274 targetID := event.TargetID{ID: id, Name: "redis"} 275 loggerOnce(context.Background(), cErr, targetID) 276 return nil, err 277 } 278 } 279 280 // Must be done after AUTH 281 if _, err = conn.Do("CLIENT", "SETNAME", "MinIO"); err != nil { 282 cErr := conn.Close() 283 targetID := event.TargetID{ID: id, Name: "redis"} 284 loggerOnce(context.Background(), cErr, targetID) 285 return nil, err 286 } 287 288 return conn, nil 289 }, 290 TestOnBorrow: func(c redis.Conn, t time.Time) error { 291 _, err := c.Do("PING") 292 return err 293 }, 294 } 295 296 var store Store 297 298 target := &RedisTarget{ 299 id: event.TargetID{ID: id, Name: "redis"}, 300 args: args, 301 pool: pool, 302 loggerOnce: loggerOnce, 303 } 304 305 if args.QueueDir != "" { 306 queueDir := filepath.Join(args.QueueDir, storePrefix+"-redis-"+id) 307 store = NewQueueStore(queueDir, args.QueueLimit) 308 if oErr := store.Open(); oErr != nil { 309 target.loggerOnce(context.Background(), oErr, target.ID()) 310 return target, oErr 311 } 312 target.store = store 313 } 314 315 conn := target.pool.Get() 316 defer func() { 317 cErr := conn.Close() 318 target.loggerOnce(context.Background(), cErr, target.ID()) 319 }() 320 321 _, pingErr := conn.Do("PING") 322 if pingErr != nil { 323 if target.store == nil || !(IsConnRefusedErr(pingErr) || IsConnResetErr(pingErr)) { 324 target.loggerOnce(context.Background(), pingErr, target.ID()) 325 return target, pingErr 326 } 327 } else { 328 if err := target.args.validateFormat(conn); err != nil { 329 target.loggerOnce(context.Background(), err, target.ID()) 330 return target, err 331 } 332 target.firstPing = true 333 } 334 335 if target.store != nil && !test { 336 // Replays the events from the store. 337 eventKeyCh := replayEvents(target.store, doneCh, target.loggerOnce, target.ID()) 338 // Start replaying events from the store. 339 go sendEvents(target, eventKeyCh, doneCh, target.loggerOnce) 340 } 341 342 return target, nil 343 }