Minor refactor to campaign manager.
- Remove external invocation of worker goroutines into Run() - Split Run() into smaller functions.
This commit is contained in:
parent
3e755596c7
commit
fd044f4cb6
|
@ -181,14 +181,116 @@ func (m *Manager) HasMessenger(id string) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run is a blocking function (and hence should be invoked as a goroutine)
|
// Run is a blocking function (that should be invoked as a goroutine)
|
||||||
// that scans the source db at regular intervals for pending campaigns,
|
// that scans the data source at regular intervals for pending campaigns,
|
||||||
// and queues them for processing. The process queue fetches batches of
|
// and queues them for processing. The process queue fetches batches of
|
||||||
// subscribers and pushes messages to them for each queued campaign
|
// subscribers and pushes messages to them for each queued campaign
|
||||||
// until all subscribers are exhausted, at which point, a campaign is marked
|
// until all subscribers are exhausted, at which point, a campaign is marked
|
||||||
// as "finished".
|
// as "finished".
|
||||||
func (m *Manager) Run(tick time.Duration) {
|
func (m *Manager) Run(tick time.Duration) {
|
||||||
go func() {
|
go m.scanCampaigns(tick)
|
||||||
|
|
||||||
|
// Spawn N message workers.
|
||||||
|
for i := 0; i < m.cfg.Concurrency; i++ {
|
||||||
|
go m.messageWorker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the next set of subscribers for a campaign and process them.
|
||||||
|
for c := range m.subFetchQueue {
|
||||||
|
has, err := m.nextSubscribers(c, m.cfg.BatchSize)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Printf("error processing campaign batch (%s): %v", c.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if has {
|
||||||
|
// There are more subscribers to fetch.
|
||||||
|
m.subFetchQueue <- c
|
||||||
|
} else if m.isCampaignProcessing(c.ID) {
|
||||||
|
// There are no more subscribers. Either the campaign status
|
||||||
|
// has changed or all subscribers have been processed.
|
||||||
|
newC, err := m.exhaustCampaign(c, "")
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Printf("error exhausting campaign (%s): %v", c.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
m.sendNotif(newC, newC.Status, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// messageWorker is a blocking function that listens to the message queue
|
||||||
|
// and pushes out incoming messages on it to the messenger.
|
||||||
|
func (m *Manager) messageWorker() {
|
||||||
|
// Counter to keep track of the message / sec rate limit.
|
||||||
|
numMsg := 0
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
// Campaign message.
|
||||||
|
case msg := <-m.campMsgQueue:
|
||||||
|
// Pause on hitting the message rate.
|
||||||
|
if numMsg >= m.cfg.MessageRate {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
numMsg = 0
|
||||||
|
}
|
||||||
|
numMsg++
|
||||||
|
|
||||||
|
err := m.messengers[msg.Campaign.MessengerID].Push(
|
||||||
|
msg.from, []string{msg.to}, msg.subject, msg.body, nil)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Printf("error sending message in campaign %s: %v", msg.Campaign.Name, err)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case m.campMsgErrorQueue <- msgError{camp: msg.Campaign, err: err}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Arbitrary message.
|
||||||
|
case msg := <-m.msgQueue:
|
||||||
|
err := m.messengers[msg.Messenger].Push(
|
||||||
|
msg.From, msg.To, msg.Subject, msg.Body, nil)
|
||||||
|
if err != nil {
|
||||||
|
m.logger.Printf("error sending message '%s': %v", msg.Subject, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TemplateFuncs returns the template functions to be applied into
|
||||||
|
// compiled campaign templates.
|
||||||
|
func (m *Manager) TemplateFuncs(c *models.Campaign) template.FuncMap {
|
||||||
|
return template.FuncMap{
|
||||||
|
"TrackLink": func(url string, msg *CampaignMessage) string {
|
||||||
|
return m.trackLink(url, msg.Campaign.UUID, msg.Subscriber.UUID)
|
||||||
|
},
|
||||||
|
"TrackView": func(msg *CampaignMessage) template.HTML {
|
||||||
|
return template.HTML(fmt.Sprintf(`<img src="%s" alt="" />`,
|
||||||
|
fmt.Sprintf(m.cfg.ViewTrackURL, msg.Campaign.UUID, msg.Subscriber.UUID)))
|
||||||
|
},
|
||||||
|
"UnsubscribeURL": func(msg *CampaignMessage) string {
|
||||||
|
return msg.unsubURL
|
||||||
|
},
|
||||||
|
"OptinURL": func(msg *CampaignMessage) string {
|
||||||
|
// Add list IDs.
|
||||||
|
// TODO: Show private lists list on optin e-mail
|
||||||
|
return fmt.Sprintf(m.cfg.OptinURL, msg.Subscriber.UUID, "")
|
||||||
|
},
|
||||||
|
"MessageURL": func(msg *CampaignMessage) string {
|
||||||
|
return fmt.Sprintf(m.cfg.MessageURL, c.UUID, msg.Subscriber.UUID)
|
||||||
|
},
|
||||||
|
"Date": func(layout string) string {
|
||||||
|
if layout == "" {
|
||||||
|
layout = time.ANSIC
|
||||||
|
}
|
||||||
|
return time.Now().Format(layout)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanCampaigns is a blocking function that periodically scans the data source
|
||||||
|
// for campaigns to process and dispatches them to the manager.
|
||||||
|
func (m *Manager) scanCampaigns(tick time.Duration) {
|
||||||
t := time.NewTicker(tick)
|
t := time.NewTicker(tick)
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -239,103 +341,6 @@ func (m *Manager) Run(tick time.Duration) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
// Fetch the next set of subscribers for a campaign and process them.
|
|
||||||
for c := range m.subFetchQueue {
|
|
||||||
has, err := m.nextSubscribers(c, m.cfg.BatchSize)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Printf("error processing campaign batch (%s): %v", c.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if has {
|
|
||||||
// There are more subscribers to fetch.
|
|
||||||
m.subFetchQueue <- c
|
|
||||||
} else if m.isCampaignProcessing(c.ID) {
|
|
||||||
// There are no more subscribers. Either the campaign status
|
|
||||||
// has changed or all subscribers have been processed.
|
|
||||||
newC, err := m.exhaustCampaign(c, "")
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Printf("error exhausting campaign (%s): %v", c.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
m.sendNotif(newC, newC.Status, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpawnWorkers spawns workers goroutines that push out messages.
|
|
||||||
func (m *Manager) SpawnWorkers() {
|
|
||||||
for i := 0; i < m.cfg.Concurrency; i++ {
|
|
||||||
go func() {
|
|
||||||
// Counter to keep track of the message / sec rate limit.
|
|
||||||
numMsg := 0
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
// Campaign message.
|
|
||||||
case msg := <-m.campMsgQueue:
|
|
||||||
// Pause on hitting the message rate.
|
|
||||||
if numMsg >= m.cfg.MessageRate {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
numMsg = 0
|
|
||||||
}
|
|
||||||
numMsg++
|
|
||||||
|
|
||||||
err := m.messengers[msg.Campaign.MessengerID].Push(
|
|
||||||
msg.from, []string{msg.to}, msg.subject, msg.body, nil)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Printf("error sending message in campaign %s: %v", msg.Campaign.Name, err)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case m.campMsgErrorQueue <- msgError{camp: msg.Campaign, err: err}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Arbitrary message.
|
|
||||||
case msg := <-m.msgQueue:
|
|
||||||
err := m.messengers[msg.Messenger].Push(
|
|
||||||
msg.From, msg.To, msg.Subject, msg.Body, nil)
|
|
||||||
if err != nil {
|
|
||||||
m.logger.Printf("error sending message '%s': %v", msg.Subject, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TemplateFuncs returns the template functions to be applied into
|
|
||||||
// compiled campaign templates.
|
|
||||||
func (m *Manager) TemplateFuncs(c *models.Campaign) template.FuncMap {
|
|
||||||
return template.FuncMap{
|
|
||||||
"TrackLink": func(url string, msg *CampaignMessage) string {
|
|
||||||
return m.trackLink(url, msg.Campaign.UUID, msg.Subscriber.UUID)
|
|
||||||
},
|
|
||||||
"TrackView": func(msg *CampaignMessage) template.HTML {
|
|
||||||
return template.HTML(fmt.Sprintf(`<img src="%s" alt="" />`,
|
|
||||||
fmt.Sprintf(m.cfg.ViewTrackURL, msg.Campaign.UUID, msg.Subscriber.UUID)))
|
|
||||||
},
|
|
||||||
"UnsubscribeURL": func(msg *CampaignMessage) string {
|
|
||||||
return msg.unsubURL
|
|
||||||
},
|
|
||||||
"OptinURL": func(msg *CampaignMessage) string {
|
|
||||||
// Add list IDs.
|
|
||||||
// TODO: Show private lists list on optin e-mail
|
|
||||||
return fmt.Sprintf(m.cfg.OptinURL, msg.Subscriber.UUID, "")
|
|
||||||
},
|
|
||||||
"MessageURL": func(msg *CampaignMessage) string {
|
|
||||||
return fmt.Sprintf(m.cfg.MessageURL, c.UUID, msg.Subscriber.UUID)
|
|
||||||
},
|
|
||||||
"Date": func(layout string) string {
|
|
||||||
if layout == "" {
|
|
||||||
layout = time.ANSIC
|
|
||||||
}
|
|
||||||
return time.Now().Format(layout)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// addCampaign adds a campaign to the process queue.
|
// addCampaign adds a campaign to the process queue.
|
||||||
|
|
1
main.go
1
main.go
|
@ -142,7 +142,6 @@ func main() {
|
||||||
// Start the campaign workers. The campaign batches (fetch from DB, push out
|
// Start the campaign workers. The campaign batches (fetch from DB, push out
|
||||||
// messages) get processed at the specified interval.
|
// messages) get processed at the specified interval.
|
||||||
go app.manager.Run(time.Second * 5)
|
go app.manager.Run(time.Second * 5)
|
||||||
app.manager.SpawnWorkers()
|
|
||||||
|
|
||||||
// Start and run the app server.
|
// Start and run the app server.
|
||||||
initHTTPServer(app)
|
initHTTPServer(app)
|
||||||
|
|
Loading…
Reference in New Issue