github.com/Financial-Times/publish-availability-monitor@v1.12.0/feeds/notificationsPushFeed.go (about) 1 package feeds 2 3 import ( 4 "bufio" 5 "encoding/json" 6 "strings" 7 "sync" 8 "time" 9 10 "github.com/Financial-Times/go-logger/v2" 11 "github.com/Financial-Times/publish-availability-monitor/httpcaller" 12 ) 13 14 const NotificationsPush = "Notifications-Push" 15 16 type NotificationsPushFeed struct { 17 baseNotificationsFeed 18 stopFeed bool 19 stopFeedLock *sync.RWMutex 20 connected bool 21 apiKey string 22 log *logger.UPPLogger 23 } 24 25 func (f *NotificationsPushFeed) Start() { 26 f.log.Infof("starting notifications-push feed from %v", f.baseURL) 27 f.stopFeedLock.Lock() 28 defer f.stopFeedLock.Unlock() 29 30 f.stopFeed = false 31 go func() { 32 if f.httpCaller == nil { 33 f.httpCaller = httpcaller.NewCaller(0) 34 } 35 36 for f.consumeFeed() { 37 time.Sleep(500 * time.Millisecond) 38 f.log.Info("Disconnected from Push feed! Attempting to reconnect.") 39 } 40 }() 41 } 42 43 func (f *NotificationsPushFeed) Stop() { 44 f.log.Infof("shutting down notifications push feed for %s", f.baseURL) 45 f.stopFeedLock.Lock() 46 defer f.stopFeedLock.Unlock() 47 48 f.stopFeed = true 49 } 50 51 func (f *NotificationsPushFeed) FeedType() string { 52 return NotificationsPush 53 } 54 55 func (f *NotificationsPushFeed) IsConnected() bool { 56 return f.connected 57 } 58 59 func (f *NotificationsPushFeed) isConsuming() bool { 60 f.stopFeedLock.RLock() 61 defer f.stopFeedLock.RUnlock() 62 63 return !f.stopFeed 64 } 65 66 func (f *NotificationsPushFeed) consumeFeed() bool { 67 tid := f.buildNotificationsTID() 68 log := f.log.WithTransactionID(tid) 69 70 resp, err := f.httpCaller.DoCall(httpcaller.Config{ 71 URL: f.baseURL, 72 Username: f.username, 73 Password: f.password, 74 APIKey: f.apiKey, 75 TID: tid, 76 }) 77 if err != nil { 78 log.WithError(err).Error("Sending request failed") 79 return f.isConsuming() 80 } 81 82 defer resp.Body.Close() 83 if resp.StatusCode != 200 { 84 log.Errorf("Received invalid statusCode: [%v]", resp.StatusCode) 85 return f.isConsuming() 86 } 87 88 log.Info("Reconnected to push feed!") 89 f.connected = true 90 defer func() { f.connected = false }() 91 92 br := bufio.NewReader(resp.Body) 93 for { 94 if !f.isConsuming() { 95 log.Info("stop consuming feed") 96 break 97 } 98 f.purgeObsoleteNotifications() 99 100 event, err := br.ReadString('\n') 101 if err != nil { 102 log.WithError(err).Info("Disconnected from push feed") 103 return f.isConsuming() 104 } 105 106 trimmed := strings.TrimSpace(event) 107 if trimmed == "" { 108 continue 109 } 110 111 data := strings.TrimPrefix(trimmed, "data: ") 112 var notifications []Notification 113 114 if err = json.Unmarshal([]byte(data), ¬ifications); err != nil { 115 log.WithError(err).Error("Error unmarshalling notifications") 116 continue 117 } 118 119 if len(notifications) == 0 { 120 continue 121 } 122 123 f.storeNotifications(notifications) 124 } 125 126 return false 127 } 128 129 func (f *NotificationsPushFeed) storeNotifications(notifications []Notification) { 130 f.notificationsLock.Lock() 131 defer f.notificationsLock.Unlock() 132 133 for _, n := range notifications { 134 uuid := parseUUIDFromURL(n.ID) 135 var history []*Notification 136 var found bool 137 if history, found = f.notifications[uuid]; !found { 138 history = make([]*Notification, 0) 139 } 140 141 // Linter warning: Implicit memory aliasing in for loop. (gosec) 142 // The implementation is based on the assumption that notification-push service 143 // will push notifications slice containing only a single item which migh lead to bugs in the future. 144 // If the "notifications" slice contains more than one item 145 // for every iteration the code will capture only the first one. 146 // nolint:gosec 147 history = append(history, &n) 148 f.notifications[uuid] = history 149 } 150 } 151 152 func (f *NotificationsPushFeed) buildNotificationsTID() string { 153 return "tid_pam_notifications_push_" + time.Now().Format(time.RFC3339) 154 }