mirror of
https://github.com/knadh/listmonk.git
synced 2025-10-06 21:36:54 +08:00
Fix race condition in immediate campaign pause-start actions.
Pausing a campaign and starting it within the interval where campaign manager pipe cleanup happens would cause a campaign to be marked as finished. This patch addresses that by only doing the 'finished' state update if the campaign wasn't stopped (cancel, pause) manually. Potentially closes #2278 as this is the only reproducible scenario where a campaign prematurely gets marked as finished.
This commit is contained in:
parent
d3c8eca446
commit
81b62a4aa9
2 changed files with 29 additions and 10 deletions
|
@ -289,8 +289,15 @@ func (m *Manager) Run() {
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Mark the pseudo counter that's added in makePipe() that is used
|
// The pipe is created with a +1 on the waitgroup pseudo counter
|
||||||
// to force a wait on a pipe.
|
// so that it immediately waits. Subsequently, every message created
|
||||||
|
// is incremented in the counter in pipe.newMessage(), and when it's'
|
||||||
|
// processed (or ignored when a campaign is paused or cancelled),
|
||||||
|
// the count is's reduced in worker().
|
||||||
|
//
|
||||||
|
// This marks down the original non-message +1, causing the waitgroup
|
||||||
|
// to be released and the pipe to end, triggering the pg.Wait()
|
||||||
|
// in newPipe() that calls pipe.cleanup().
|
||||||
p.wg.Done()
|
p.wg.Done()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -439,8 +446,9 @@ func (m *Manager) worker() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the campaign has ended, ignore the message.
|
// If the campaign has ended or stopped, ignore the message.
|
||||||
if msg.pipe != nil && msg.pipe.stopped.Load() {
|
if msg.pipe != nil && msg.pipe.stopped.Load() {
|
||||||
|
// Reduce the message counter on the pipe.
|
||||||
msg.pipe.wg.Done()
|
msg.pipe.wg.Done()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,7 +60,6 @@ func (m *Manager) newPipe(c *models.Campaign) (*pipe, error) {
|
||||||
// (successfully or skipped after errors or cancellation).
|
// (successfully or skipped after errors or cancellation).
|
||||||
p.wg.Wait()
|
p.wg.Wait()
|
||||||
|
|
||||||
p.Stop(false)
|
|
||||||
p.cleanup()
|
p.cleanup()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -75,13 +74,14 @@ func (m *Manager) newPipe(c *models.Campaign) (*pipe, error) {
|
||||||
// in the current batch or not. A false indicates that all subscribers
|
// in the current batch or not. A false indicates that all subscribers
|
||||||
// have been processed, or that a campaign has been paused or cancelled.
|
// have been processed, or that a campaign has been paused or cancelled.
|
||||||
func (p *pipe) NextSubscribers() (bool, error) {
|
func (p *pipe) NextSubscribers() (bool, error) {
|
||||||
// Fetch a batch of subscribers.
|
// Fetch the next batch of subscribers from a 'running' campaign.
|
||||||
subs, err := p.m.store.NextSubscribers(p.camp.ID, p.m.cfg.BatchSize)
|
subs, err := p.m.store.NextSubscribers(p.camp.ID, p.m.cfg.BatchSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error fetching campaign subscribers (%s): %v", p.camp.Name, err)
|
return false, fmt.Errorf("error fetching campaign subscribers (%s): %v", p.camp.Name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// There are no subscribers.
|
// There are no subscribers from the query. Either all subscribers on the campaign
|
||||||
|
// have been processed, or the campaign has changed from 'running' to 'paused' or 'cancelled'.
|
||||||
if len(subs) == 0 {
|
if len(subs) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
@ -167,6 +167,9 @@ func (p *pipe) Stop(withErrors bool) {
|
||||||
p.stopped.Store(true)
|
p.stopped.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newMessage returns a campaign message while internally incrementing the
|
||||||
|
// number of messages in the pipe wait group so that the status of every
|
||||||
|
// message can be atomically tracked.
|
||||||
func (p *pipe) newMessage(s models.Subscriber) (CampaignMessage, error) {
|
func (p *pipe) newMessage(s models.Subscriber) (CampaignMessage, error) {
|
||||||
msg, err := p.m.NewCampaignMessage(p.camp, s)
|
msg, err := p.m.NewCampaignMessage(p.camp, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -180,7 +183,8 @@ func (p *pipe) newMessage(s models.Subscriber) (CampaignMessage, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup finishes the campaign and updates the campaign status in the DB
|
// cleanup finishes the campaign and updates the campaign status in the DB
|
||||||
// and also triggers a notification to the admin.
|
// and also triggers a notification to the admin. This only triggers once
|
||||||
|
// a pipe's wg counter is fully exhausted, draining all messages in its queue.
|
||||||
func (p *pipe) cleanup() {
|
func (p *pipe) cleanup() {
|
||||||
defer func() {
|
defer func() {
|
||||||
p.m.pipesMut.Lock()
|
p.m.pipesMut.Lock()
|
||||||
|
@ -188,7 +192,7 @@ func (p *pipe) cleanup() {
|
||||||
p.m.pipesMut.Unlock()
|
p.m.pipesMut.Unlock()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Update campaign's "sent" count.
|
// Update campaign's 'sent count.
|
||||||
if err := p.m.store.UpdateCampaignCounts(p.camp.ID, 0, int(p.sent.Load()), int(p.lastID.Load())); err != nil {
|
if err := p.m.store.UpdateCampaignCounts(p.camp.ID, 0, int(p.sent.Load()), int(p.lastID.Load())); err != nil {
|
||||||
p.m.log.Printf("error updating campaign counts (%s): %v", p.camp.Name, err)
|
p.m.log.Printf("error updating campaign counts (%s): %v", p.camp.Name, err)
|
||||||
}
|
}
|
||||||
|
@ -205,6 +209,13 @@ func (p *pipe) cleanup() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The campaign was manually stopped (pause, cancel).
|
||||||
|
if p.stopped.Load() {
|
||||||
|
p.m.log.Printf("stop processing campaign (%s)", p.camp.Name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Campaign wasn't manually stopped and subscribers were naturally exhausted.
|
||||||
// Fetch the up-to-date campaign status from the DB.
|
// Fetch the up-to-date campaign status from the DB.
|
||||||
c, err := p.m.store.GetCampaign(p.camp.ID)
|
c, err := p.m.store.GetCampaign(p.camp.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -221,9 +232,9 @@ func (p *pipe) cleanup() {
|
||||||
p.m.log.Printf("campaign (%s) finished", p.camp.Name)
|
p.m.log.Printf("campaign (%s) finished", p.camp.Name)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.m.log.Printf("stop processing campaign (%s)", p.camp.Name)
|
p.m.log.Printf("finish processing campaign (%s)", p.camp.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the admin.
|
// Notify admin.
|
||||||
_ = p.m.sendNotif(c, c.Status, "")
|
_ = p.m.sendNotif(c, c.Status, "")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue