github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/amqp.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" 26 "net/url" 27 "os" 28 "path/filepath" 29 "sync" 30 31 "github.com/minio/minio/internal/event" 32 "github.com/minio/minio/internal/logger" 33 "github.com/minio/minio/internal/once" 34 "github.com/minio/minio/internal/store" 35 xnet "github.com/minio/pkg/v2/net" 36 "github.com/rabbitmq/amqp091-go" 37 ) 38 39 // AMQPArgs - AMQP target arguments. 40 type AMQPArgs struct { 41 Enable bool `json:"enable"` 42 URL xnet.URL `json:"url"` 43 Exchange string `json:"exchange"` 44 RoutingKey string `json:"routingKey"` 45 ExchangeType string `json:"exchangeType"` 46 DeliveryMode uint8 `json:"deliveryMode"` 47 Mandatory bool `json:"mandatory"` 48 Immediate bool `json:"immediate"` 49 Durable bool `json:"durable"` 50 Internal bool `json:"internal"` 51 NoWait bool `json:"noWait"` 52 AutoDeleted bool `json:"autoDeleted"` 53 PublisherConfirms bool `json:"publisherConfirms"` 54 QueueDir string `json:"queueDir"` 55 QueueLimit uint64 `json:"queueLimit"` 56 } 57 58 //lint:file-ignore ST1003 We cannot change these exported names. 59 60 // AMQP input constants. 61 const ( 62 AmqpQueueDir = "queue_dir" 63 AmqpQueueLimit = "queue_limit" 64 65 AmqpURL = "url" 66 AmqpExchange = "exchange" 67 AmqpRoutingKey = "routing_key" 68 AmqpExchangeType = "exchange_type" 69 AmqpDeliveryMode = "delivery_mode" 70 AmqpMandatory = "mandatory" 71 AmqpImmediate = "immediate" 72 AmqpDurable = "durable" 73 AmqpInternal = "internal" 74 AmqpNoWait = "no_wait" 75 AmqpAutoDeleted = "auto_deleted" 76 AmqpArguments = "arguments" 77 AmqpPublisherConfirms = "publisher_confirms" 78 79 EnvAMQPEnable = "MINIO_NOTIFY_AMQP_ENABLE" 80 EnvAMQPURL = "MINIO_NOTIFY_AMQP_URL" 81 EnvAMQPExchange = "MINIO_NOTIFY_AMQP_EXCHANGE" 82 EnvAMQPRoutingKey = "MINIO_NOTIFY_AMQP_ROUTING_KEY" 83 EnvAMQPExchangeType = "MINIO_NOTIFY_AMQP_EXCHANGE_TYPE" 84 EnvAMQPDeliveryMode = "MINIO_NOTIFY_AMQP_DELIVERY_MODE" 85 EnvAMQPMandatory = "MINIO_NOTIFY_AMQP_MANDATORY" 86 EnvAMQPImmediate = "MINIO_NOTIFY_AMQP_IMMEDIATE" 87 EnvAMQPDurable = "MINIO_NOTIFY_AMQP_DURABLE" 88 EnvAMQPInternal = "MINIO_NOTIFY_AMQP_INTERNAL" 89 EnvAMQPNoWait = "MINIO_NOTIFY_AMQP_NO_WAIT" 90 EnvAMQPAutoDeleted = "MINIO_NOTIFY_AMQP_AUTO_DELETED" 91 EnvAMQPArguments = "MINIO_NOTIFY_AMQP_ARGUMENTS" 92 EnvAMQPPublisherConfirms = "MINIO_NOTIFY_AMQP_PUBLISHING_CONFIRMS" 93 EnvAMQPQueueDir = "MINIO_NOTIFY_AMQP_QUEUE_DIR" 94 EnvAMQPQueueLimit = "MINIO_NOTIFY_AMQP_QUEUE_LIMIT" 95 ) 96 97 // Validate AMQP arguments 98 func (a *AMQPArgs) Validate() error { 99 if !a.Enable { 100 return nil 101 } 102 if _, err := amqp091.ParseURI(a.URL.String()); err != nil { 103 return err 104 } 105 if a.QueueDir != "" { 106 if !filepath.IsAbs(a.QueueDir) { 107 return errors.New("queueDir path should be absolute") 108 } 109 } 110 111 return nil 112 } 113 114 // AMQPTarget - AMQP target 115 type AMQPTarget struct { 116 initOnce once.Init 117 118 id event.TargetID 119 args AMQPArgs 120 conn *amqp091.Connection 121 connMutex sync.Mutex 122 store store.Store[event.Event] 123 loggerOnce logger.LogOnce 124 125 quitCh chan struct{} 126 } 127 128 // ID - returns TargetID. 129 func (target *AMQPTarget) ID() event.TargetID { 130 return target.id 131 } 132 133 // Name - returns the Name of the target. 134 func (target *AMQPTarget) Name() string { 135 return target.ID().String() 136 } 137 138 // Store returns any underlying store if set. 139 func (target *AMQPTarget) Store() event.TargetStore { 140 return target.store 141 } 142 143 // IsActive - Return true if target is up and active 144 func (target *AMQPTarget) IsActive() (bool, error) { 145 if err := target.init(); err != nil { 146 return false, err 147 } 148 149 return target.isActive() 150 } 151 152 func (target *AMQPTarget) isActive() (bool, error) { 153 ch, _, err := target.channel() 154 if err != nil { 155 return false, err 156 } 157 defer func() { 158 ch.Close() 159 }() 160 return true, nil 161 } 162 163 func (target *AMQPTarget) channel() (*amqp091.Channel, chan amqp091.Confirmation, error) { 164 var err error 165 var conn *amqp091.Connection 166 var ch *amqp091.Channel 167 168 isAMQPClosedErr := func(err error) bool { 169 if err == amqp091.ErrClosed { 170 return true 171 } 172 173 if nerr, ok := err.(*net.OpError); ok { 174 return (nerr.Err.Error() == "use of closed network connection") 175 } 176 177 return false 178 } 179 180 target.connMutex.Lock() 181 defer target.connMutex.Unlock() 182 183 if target.conn != nil { 184 ch, err = target.conn.Channel() 185 if err == nil { 186 if target.args.PublisherConfirms { 187 confirms := ch.NotifyPublish(make(chan amqp091.Confirmation, 1)) 188 if err := ch.Confirm(false); err != nil { 189 ch.Close() 190 return nil, nil, err 191 } 192 return ch, confirms, nil 193 } 194 return ch, nil, nil 195 } 196 197 if !isAMQPClosedErr(err) { 198 return nil, nil, err 199 } 200 201 // close when we know this is a network error. 202 target.conn.Close() 203 } 204 205 conn, err = amqp091.Dial(target.args.URL.String()) 206 if err != nil { 207 if xnet.IsConnRefusedErr(err) { 208 return nil, nil, store.ErrNotConnected 209 } 210 return nil, nil, err 211 } 212 213 ch, err = conn.Channel() 214 if err != nil { 215 return nil, nil, err 216 } 217 218 target.conn = conn 219 220 if target.args.PublisherConfirms { 221 confirms := ch.NotifyPublish(make(chan amqp091.Confirmation, 1)) 222 if err := ch.Confirm(false); err != nil { 223 ch.Close() 224 return nil, nil, err 225 } 226 return ch, confirms, nil 227 } 228 229 return ch, nil, nil 230 } 231 232 // send - sends an event to the AMQP091. 233 func (target *AMQPTarget) send(eventData event.Event, ch *amqp091.Channel, confirms chan amqp091.Confirmation) error { 234 objectName, err := url.QueryUnescape(eventData.S3.Object.Key) 235 if err != nil { 236 return err 237 } 238 key := eventData.S3.Bucket.Name + "/" + objectName 239 240 data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}}) 241 if err != nil { 242 return err 243 } 244 245 headers := make(amqp091.Table) 246 // Add more information here as required, but be aware to not overload headers 247 headers["minio-bucket"] = eventData.S3.Bucket.Name 248 headers["minio-event"] = eventData.EventName.String() 249 250 if err = ch.ExchangeDeclare(target.args.Exchange, target.args.ExchangeType, target.args.Durable, 251 target.args.AutoDeleted, target.args.Internal, target.args.NoWait, nil); err != nil { 252 return err 253 } 254 255 if err = ch.Publish(target.args.Exchange, target.args.RoutingKey, target.args.Mandatory, 256 target.args.Immediate, amqp091.Publishing{ 257 Headers: headers, 258 ContentType: "application/json", 259 DeliveryMode: target.args.DeliveryMode, 260 Body: data, 261 }); err != nil { 262 return err 263 } 264 265 // check for publisher confirms only if its enabled 266 if target.args.PublisherConfirms { 267 confirmed := <-confirms 268 if !confirmed.Ack { 269 return fmt.Errorf("failed delivery of delivery tag: %d", confirmed.DeliveryTag) 270 } 271 } 272 273 return nil 274 } 275 276 // Save - saves the events to the store which will be replayed when the amqp connection is active. 277 func (target *AMQPTarget) Save(eventData event.Event) error { 278 if target.store != nil { 279 return target.store.Put(eventData) 280 } 281 if err := target.init(); err != nil { 282 return err 283 } 284 ch, confirms, err := target.channel() 285 if err != nil { 286 return err 287 } 288 defer ch.Close() 289 290 return target.send(eventData, ch, confirms) 291 } 292 293 // SendFromStore - reads an event from store and sends it to AMQP091. 294 func (target *AMQPTarget) SendFromStore(key store.Key) error { 295 if err := target.init(); err != nil { 296 return err 297 } 298 299 ch, confirms, err := target.channel() 300 if err != nil { 301 return err 302 } 303 defer ch.Close() 304 305 eventData, eErr := target.store.Get(key.Name) 306 if eErr != nil { 307 // The last event key in a successful batch will be sent in the channel atmost once by the replayEvents() 308 // Such events will not exist and wouldve been already been sent successfully. 309 if os.IsNotExist(eErr) { 310 return nil 311 } 312 return eErr 313 } 314 315 if err := target.send(eventData, ch, confirms); err != nil { 316 return err 317 } 318 319 // Delete the event from store. 320 return target.store.Del(key.Name) 321 } 322 323 // Close - does nothing and available for interface compatibility. 324 func (target *AMQPTarget) Close() error { 325 close(target.quitCh) 326 if target.conn != nil { 327 return target.conn.Close() 328 } 329 return nil 330 } 331 332 func (target *AMQPTarget) init() error { 333 return target.initOnce.Do(target.initAMQP) 334 } 335 336 func (target *AMQPTarget) initAMQP() error { 337 conn, err := amqp091.Dial(target.args.URL.String()) 338 if err != nil { 339 if xnet.IsConnRefusedErr(err) || xnet.IsConnResetErr(err) { 340 target.loggerOnce(context.Background(), err, target.ID().String()) 341 } 342 return err 343 } 344 target.conn = conn 345 346 return nil 347 } 348 349 // NewAMQPTarget - creates new AMQP target. 350 func NewAMQPTarget(id string, args AMQPArgs, loggerOnce logger.LogOnce) (*AMQPTarget, error) { 351 var queueStore store.Store[event.Event] 352 if args.QueueDir != "" { 353 queueDir := filepath.Join(args.QueueDir, storePrefix+"-amqp-"+id) 354 queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension) 355 if err := queueStore.Open(); err != nil { 356 return nil, fmt.Errorf("unable to initialize the queue store of AMQP `%s`: %w", id, err) 357 } 358 } 359 360 target := &AMQPTarget{ 361 id: event.TargetID{ID: id, Name: "amqp"}, 362 args: args, 363 loggerOnce: loggerOnce, 364 store: queueStore, 365 quitCh: make(chan struct{}), 366 } 367 368 if target.store != nil { 369 store.StreamItems(target.store, target, target.quitCh, target.loggerOnce) 370 } 371 372 return target, nil 373 }