github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/event/target/nsq.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 "crypto/tls" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "net/url" 27 "os" 28 "path/filepath" 29 30 "github.com/nsqio/go-nsq" 31 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 // NSQ constants 40 const ( 41 NSQAddress = "nsqd_address" 42 NSQTopic = "topic" 43 NSQTLS = "tls" 44 NSQTLSSkipVerify = "tls_skip_verify" 45 NSQQueueDir = "queue_dir" 46 NSQQueueLimit = "queue_limit" 47 48 EnvNSQEnable = "MINIO_NOTIFY_NSQ_ENABLE" 49 EnvNSQAddress = "MINIO_NOTIFY_NSQ_NSQD_ADDRESS" 50 EnvNSQTopic = "MINIO_NOTIFY_NSQ_TOPIC" 51 EnvNSQTLS = "MINIO_NOTIFY_NSQ_TLS" 52 EnvNSQTLSSkipVerify = "MINIO_NOTIFY_NSQ_TLS_SKIP_VERIFY" 53 EnvNSQQueueDir = "MINIO_NOTIFY_NSQ_QUEUE_DIR" 54 EnvNSQQueueLimit = "MINIO_NOTIFY_NSQ_QUEUE_LIMIT" 55 ) 56 57 // NSQArgs - NSQ target arguments. 58 type NSQArgs struct { 59 Enable bool `json:"enable"` 60 NSQDAddress xnet.Host `json:"nsqdAddress"` 61 Topic string `json:"topic"` 62 TLS struct { 63 Enable bool `json:"enable"` 64 SkipVerify bool `json:"skipVerify"` 65 } `json:"tls"` 66 QueueDir string `json:"queueDir"` 67 QueueLimit uint64 `json:"queueLimit"` 68 } 69 70 // Validate NSQArgs fields 71 func (n NSQArgs) Validate() error { 72 if !n.Enable { 73 return nil 74 } 75 76 if n.NSQDAddress.IsEmpty() { 77 return errors.New("empty nsqdAddress") 78 } 79 80 if n.Topic == "" { 81 return errors.New("empty topic") 82 } 83 if n.QueueDir != "" { 84 if !filepath.IsAbs(n.QueueDir) { 85 return errors.New("queueDir path should be absolute") 86 } 87 } 88 89 return nil 90 } 91 92 // NSQTarget - NSQ target. 93 type NSQTarget struct { 94 initOnce once.Init 95 96 id event.TargetID 97 args NSQArgs 98 producer *nsq.Producer 99 store store.Store[event.Event] 100 config *nsq.Config 101 loggerOnce logger.LogOnce 102 quitCh chan struct{} 103 } 104 105 // ID - returns target ID. 106 func (target *NSQTarget) ID() event.TargetID { 107 return target.id 108 } 109 110 // Name - returns the Name of the target. 111 func (target *NSQTarget) Name() string { 112 return target.ID().String() 113 } 114 115 // Store returns any underlying store if set. 116 func (target *NSQTarget) Store() event.TargetStore { 117 return target.store 118 } 119 120 // IsActive - Return true if target is up and active 121 func (target *NSQTarget) IsActive() (bool, error) { 122 if err := target.init(); err != nil { 123 return false, err 124 } 125 return target.isActive() 126 } 127 128 func (target *NSQTarget) isActive() (bool, error) { 129 if target.producer == nil { 130 producer, err := nsq.NewProducer(target.args.NSQDAddress.String(), target.config) 131 if err != nil { 132 return false, err 133 } 134 target.producer = producer 135 } 136 137 if err := target.producer.Ping(); err != nil { 138 // To treat "connection refused" errors as errNotConnected. 139 if xnet.IsConnRefusedErr(err) { 140 return false, store.ErrNotConnected 141 } 142 return false, err 143 } 144 return true, nil 145 } 146 147 // Save - saves the events to the store which will be replayed when the nsq connection is active. 148 func (target *NSQTarget) Save(eventData event.Event) error { 149 if target.store != nil { 150 return target.store.Put(eventData) 151 } 152 153 if err := target.init(); err != nil { 154 return err 155 } 156 157 _, err := target.isActive() 158 if err != nil { 159 return err 160 } 161 return target.send(eventData) 162 } 163 164 // send - sends an event to the NSQ. 165 func (target *NSQTarget) send(eventData event.Event) error { 166 objectName, err := url.QueryUnescape(eventData.S3.Object.Key) 167 if err != nil { 168 return err 169 } 170 key := eventData.S3.Bucket.Name + "/" + objectName 171 172 data, err := json.Marshal(event.Log{EventName: eventData.EventName, Key: key, Records: []event.Event{eventData}}) 173 if err != nil { 174 return err 175 } 176 177 return target.producer.Publish(target.args.Topic, data) 178 } 179 180 // SendFromStore - reads an event from store and sends it to NSQ. 181 func (target *NSQTarget) SendFromStore(key store.Key) error { 182 if err := target.init(); err != nil { 183 return err 184 } 185 186 _, err := target.isActive() 187 if err != nil { 188 return err 189 } 190 191 eventData, eErr := target.store.Get(key.Name) 192 if eErr != nil { 193 // The last event key in a successful batch will be sent in the channel atmost once by the replayEvents() 194 // Such events will not exist and wouldve been already been sent successfully. 195 if os.IsNotExist(eErr) { 196 return nil 197 } 198 return eErr 199 } 200 201 if err := target.send(eventData); err != nil { 202 return err 203 } 204 205 // Delete the event from store. 206 return target.store.Del(key.Name) 207 } 208 209 // Close - closes underneath connections to NSQD server. 210 func (target *NSQTarget) Close() (err error) { 211 close(target.quitCh) 212 if target.producer != nil { 213 // this blocks until complete: 214 target.producer.Stop() 215 } 216 return nil 217 } 218 219 func (target *NSQTarget) init() error { 220 return target.initOnce.Do(target.initNSQ) 221 } 222 223 func (target *NSQTarget) initNSQ() error { 224 args := target.args 225 226 config := nsq.NewConfig() 227 if args.TLS.Enable { 228 config.TlsV1 = true 229 config.TlsConfig = &tls.Config{ 230 InsecureSkipVerify: args.TLS.SkipVerify, 231 } 232 } 233 target.config = config 234 235 producer, err := nsq.NewProducer(args.NSQDAddress.String(), config) 236 if err != nil { 237 target.loggerOnce(context.Background(), err, target.ID().String()) 238 return err 239 } 240 target.producer = producer 241 242 err = target.producer.Ping() 243 if err != nil { 244 // To treat "connection refused" errors as errNotConnected. 245 if !(xnet.IsConnRefusedErr(err) || xnet.IsConnResetErr(err)) { 246 target.loggerOnce(context.Background(), err, target.ID().String()) 247 } 248 target.producer.Stop() 249 return err 250 } 251 252 yes, err := target.isActive() 253 if err != nil { 254 return err 255 } 256 if !yes { 257 return store.ErrNotConnected 258 } 259 260 return nil 261 } 262 263 // NewNSQTarget - creates new NSQ target. 264 func NewNSQTarget(id string, args NSQArgs, loggerOnce logger.LogOnce) (*NSQTarget, error) { 265 var queueStore store.Store[event.Event] 266 if args.QueueDir != "" { 267 queueDir := filepath.Join(args.QueueDir, storePrefix+"-nsq-"+id) 268 queueStore = store.NewQueueStore[event.Event](queueDir, args.QueueLimit, event.StoreExtension) 269 if err := queueStore.Open(); err != nil { 270 return nil, fmt.Errorf("unable to initialize the queue store of NSQ `%s`: %w", id, err) 271 } 272 } 273 274 target := &NSQTarget{ 275 id: event.TargetID{ID: id, Name: "nsq"}, 276 args: args, 277 loggerOnce: loggerOnce, 278 store: queueStore, 279 quitCh: make(chan struct{}), 280 } 281 282 if target.store != nil { 283 store.StreamItems(target.store, target, target.quitCh, target.loggerOnce) 284 } 285 286 return target, nil 287 }