storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/event/target/amqp.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 "net" 24 "net/url" 25 "os" 26 "path/filepath" 27 "sync" 28 29 "github.com/streadway/amqp" 30 31 "storj.io/minio/pkg/event" 32 xnet "storj.io/minio/pkg/net" 33 ) 34 35 // AMQPArgs - AMQP target arguments. 36 type AMQPArgs struct { 37 Enable bool `json:"enable"` 38 URL xnet.URL `json:"url"` 39 Exchange string `json:"exchange"` 40 RoutingKey string `json:"routingKey"` 41 ExchangeType string `json:"exchangeType"` 42 DeliveryMode uint8 `json:"deliveryMode"` 43 Mandatory bool `json:"mandatory"` 44 Immediate bool `json:"immediate"` 45 Durable bool `json:"durable"` 46 Internal bool `json:"internal"` 47 NoWait bool `json:"noWait"` 48 AutoDeleted bool `json:"autoDeleted"` 49 QueueDir string `json:"queueDir"` 50 QueueLimit uint64 `json:"queueLimit"` 51 } 52 53 //lint:file-ignore ST1003 We cannot change these exported names. 54 55 // AMQP input constants. 56 const ( 57 AmqpQueueDir = "queue_dir" 58 AmqpQueueLimit = "queue_limit" 59 60 AmqpURL = "url" 61 AmqpExchange = "exchange" 62 AmqpRoutingKey = "routing_key" 63 AmqpExchangeType = "exchange_type" 64 AmqpDeliveryMode = "delivery_mode" 65 AmqpMandatory = "mandatory" 66 AmqpImmediate = "immediate" 67 AmqpDurable = "durable" 68 AmqpInternal = "internal" 69 AmqpNoWait = "no_wait" 70 AmqpAutoDeleted = "auto_deleted" 71 AmqpArguments = "arguments" 72 AmqpPublishingHeaders = "publishing_headers" 73 74 EnvAMQPEnable = "MINIO_NOTIFY_AMQP_ENABLE" 75 EnvAMQPURL = "MINIO_NOTIFY_AMQP_URL" 76 EnvAMQPExchange = "MINIO_NOTIFY_AMQP_EXCHANGE" 77 EnvAMQPRoutingKey = "MINIO_NOTIFY_AMQP_ROUTING_KEY" 78 EnvAMQPExchangeType = "MINIO_NOTIFY_AMQP_EXCHANGE_TYPE" 79 EnvAMQPDeliveryMode = "MINIO_NOTIFY_AMQP_DELIVERY_MODE" 80 EnvAMQPMandatory = "MINIO_NOTIFY_AMQP_MANDATORY" 81 EnvAMQPImmediate = "MINIO_NOTIFY_AMQP_IMMEDIATE" 82 EnvAMQPDurable = "MINIO_NOTIFY_AMQP_DURABLE" 83 EnvAMQPInternal = "MINIO_NOTIFY_AMQP_INTERNAL" 84 EnvAMQPNoWait = "MINIO_NOTIFY_AMQP_NO_WAIT" 85 EnvAMQPAutoDeleted = "MINIO_NOTIFY_AMQP_AUTO_DELETED" 86 EnvAMQPArguments = "MINIO_NOTIFY_AMQP_ARGUMENTS" 87 EnvAMQPPublishingHeaders = "MINIO_NOTIFY_AMQP_PUBLISHING_HEADERS" 88 EnvAMQPQueueDir = "MINIO_NOTIFY_AMQP_QUEUE_DIR" 89 EnvAMQPQueueLimit = "MINIO_NOTIFY_AMQP_QUEUE_LIMIT" 90 ) 91 92 // Validate AMQP arguments 93 func (a *AMQPArgs) Validate() error { 94 if !a.Enable { 95 return nil 96 } 97 if _, err := amqp.ParseURI(a.URL.String()); err != nil { 98 return err 99 } 100 if a.QueueDir != "" { 101 if !filepath.IsAbs(a.QueueDir) { 102 return errors.New("queueDir path should be absolute") 103 } 104 } 105 106 return nil 107 } 108 109 // AMQPTarget - AMQP target 110 type AMQPTarget struct { 111 id event.TargetID 112 args AMQPArgs 113 conn *amqp.Connection 114 connMutex sync.Mutex 115 store Store 116 loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}) 117 } 118 119 // ID - returns TargetID. 120 func (target *AMQPTarget) ID() event.TargetID { 121 return target.id 122 } 123 124 // IsActive - Return true if target is up and active 125 func (target *AMQPTarget) IsActive() (bool, error) { 126 ch, err := target.channel() 127 if err != nil { 128 return false, err 129 } 130 defer func() { 131 ch.Close() 132 }() 133 return true, nil 134 } 135 136 // HasQueueStore - Checks if the queueStore has been configured for the target 137 func (target *AMQPTarget) HasQueueStore() bool { 138 return target.store != nil 139 } 140 141 func (target *AMQPTarget) channel() (*amqp.Channel, error) { 142 var err error 143 var conn *amqp.Connection 144 var ch *amqp.Channel 145 146 isAMQPClosedErr := func(err error) bool { 147 if err == amqp.ErrClosed { 148 return true 149 } 150 151 if nerr, ok := err.(*net.OpError); ok { 152 return (nerr.Err.Error() == "use of closed network connection") 153 } 154 155 return false 156 } 157 158 target.connMutex.Lock() 159 defer target.connMutex.Unlock() 160 161 if target.conn != nil { 162 ch, err = target.conn.Channel() 163 if err == nil { 164 return ch, nil 165 } 166 167 if !isAMQPClosedErr(err) { 168 return nil, err 169 } 170 } 171 172 conn, err = amqp.Dial(target.args.URL.String()) 173 if err != nil { 174 if IsConnRefusedErr(err) { 175 return nil, errNotConnected 176 } 177 return nil, err 178 } 179 180 ch, err = conn.Channel() 181 if err != nil { 182 return nil, err 183 } 184 185 target.conn = conn 186 187 return ch, nil 188 } 189 190 // send - sends an event to the AMQP. 191 func (target *AMQPTarget) send(eventData event.Event, ch *amqp.Channel) error { 192 objectName, err := url.QueryUnescape(eventData.S3.Object.Key) 193 if err != nil { 194 return err 195 } 196 key := eventData.S3.Bucket.Name + "/" + objectName 197 198 data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}}) 199 if err != nil { 200 return err 201 } 202 203 if err = ch.ExchangeDeclare(target.args.Exchange, target.args.ExchangeType, target.args.Durable, 204 target.args.AutoDeleted, target.args.Internal, target.args.NoWait, nil); err != nil { 205 return err 206 } 207 208 if err := ch.Publish(target.args.Exchange, target.args.RoutingKey, target.args.Mandatory, 209 target.args.Immediate, amqp.Publishing{ 210 ContentType: "application/json", 211 DeliveryMode: target.args.DeliveryMode, 212 Body: data, 213 }); err != nil { 214 return err 215 } 216 217 return nil 218 } 219 220 // Save - saves the events to the store which will be replayed when the amqp connection is active. 221 func (target *AMQPTarget) Save(eventData event.Event) error { 222 if target.store != nil { 223 return target.store.Put(eventData) 224 } 225 ch, err := target.channel() 226 if err != nil { 227 return err 228 } 229 defer func() { 230 cErr := ch.Close() 231 target.loggerOnce(context.Background(), cErr, target.ID()) 232 }() 233 234 return target.send(eventData, ch) 235 } 236 237 // Send - sends event to AMQP. 238 func (target *AMQPTarget) Send(eventKey string) error { 239 ch, err := target.channel() 240 if err != nil { 241 return err 242 } 243 defer func() { 244 cErr := ch.Close() 245 target.loggerOnce(context.Background(), cErr, target.ID()) 246 }() 247 248 eventData, eErr := target.store.Get(eventKey) 249 if eErr != nil { 250 // The last event key in a successful batch will be sent in the channel atmost once by the replayEvents() 251 // Such events will not exist and wouldve been already been sent successfully. 252 if os.IsNotExist(eErr) { 253 return nil 254 } 255 return eErr 256 } 257 258 if err := target.send(eventData, ch); err != nil { 259 return err 260 } 261 262 // Delete the event from store. 263 return target.store.Del(eventKey) 264 } 265 266 // Close - does nothing and available for interface compatibility. 267 func (target *AMQPTarget) Close() error { 268 if target.conn != nil { 269 return target.conn.Close() 270 } 271 return nil 272 } 273 274 // NewAMQPTarget - creates new AMQP target. 275 func NewAMQPTarget(id string, args AMQPArgs, doneCh <-chan struct{}, loggerOnce func(ctx context.Context, err error, id interface{}, errKind ...interface{}), test bool) (*AMQPTarget, error) { 276 var conn *amqp.Connection 277 var err error 278 279 var store Store 280 281 target := &AMQPTarget{ 282 id: event.TargetID{ID: id, Name: "amqp"}, 283 args: args, 284 loggerOnce: loggerOnce, 285 } 286 287 if args.QueueDir != "" { 288 queueDir := filepath.Join(args.QueueDir, storePrefix+"-amqp-"+id) 289 store = NewQueueStore(queueDir, args.QueueLimit) 290 if oErr := store.Open(); oErr != nil { 291 target.loggerOnce(context.Background(), oErr, target.ID()) 292 return target, oErr 293 } 294 target.store = store 295 } 296 297 conn, err = amqp.Dial(args.URL.String()) 298 if err != nil { 299 if store == nil || !(IsConnRefusedErr(err) || IsConnResetErr(err)) { 300 target.loggerOnce(context.Background(), err, target.ID()) 301 return target, err 302 } 303 } 304 target.conn = conn 305 306 if target.store != nil && !test { 307 // Replays the events from the store. 308 eventKeyCh := replayEvents(target.store, doneCh, target.loggerOnce, target.ID()) 309 310 // Start replaying events from the store. 311 go sendEvents(target, eventKeyCh, doneCh, target.loggerOnce) 312 } 313 314 return target, nil 315 }